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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ require (
github.com/google/go-containerregistry v0.20.7
github.com/gorilla/websocket v1.5.3
github.com/itchyny/json2yaml v0.1.4
github.com/kernel/hypeman-go v0.9.6
github.com/kernel/hypeman-go v0.9.7-0.20260211203915-a9a0d6c96059
github.com/muesli/reflow v0.3.0
github.com/stretchr/testify v1.11.1
github.com/tidwall/gjson v1.18.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnV
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/itchyny/json2yaml v0.1.4 h1:/pErVOXGG5iTyXHi/QKR4y3uzhLjGTEmmJIy97YT+k8=
github.com/itchyny/json2yaml v0.1.4/go.mod h1:6iudhBZdarpjLFRNj+clWLAkGft+9uCcjAZYXUH9eGI=
github.com/kernel/hypeman-go v0.9.6 h1:gkKbUiTYPWVDa9GwX/xaf9+z+eiTYIj6oglPlir4Xbo=
github.com/kernel/hypeman-go v0.9.6/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI=
github.com/kernel/hypeman-go v0.9.7-0.20260211203915-a9a0d6c96059 h1:C5ixkSnUllJJSWBVAusdj8uX7FcS/eJE1TbzqGAvwQc=
github.com/kernel/hypeman-go v0.9.7-0.20260211203915-a9a0d6c96059/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
Expand Down
166 changes: 166 additions & 0 deletions pkg/cmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/kernel/hypeman-go"
"github.com/kernel/hypeman-go/option"
"github.com/tidwall/gjson"
"github.com/urfave/cli/v3"
)

Expand All @@ -24,6 +25,11 @@ var buildCmd = cli.Command{
The path argument specifies the build context directory containing the
source code and Dockerfile. If not specified, the current directory is used.

Subcommands are available for managing builds:
hypeman build list List builds
hypeman build get <id> Get build details
hypeman build cancel <id> Cancel a build

Examples:
# Build from current directory
hypeman build
Expand All @@ -48,6 +54,11 @@ Examples:
Value: 600,
},
},
Commands: []*cli.Command{
&buildListCmd,
&buildGetCmd,
&buildCancelCmd,
},
Action: handleBuild,
HideHelpCommand: true,
}
Expand Down Expand Up @@ -281,3 +292,158 @@ func createSourceTarball(contextPath string) (*bytes.Buffer, error) {

return buf, nil
}

var buildListCmd = cli.Command{
Name: "list",
Usage: "List builds",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "quiet",
Aliases: []string{"q"},
Usage: "Only display build IDs",
},
},
Action: handleBuildList,
HideHelpCommand: true,
}

var buildGetCmd = cli.Command{
Name: "get",
Usage: "Get build details",
ArgsUsage: "<id>",
Action: handleBuildGet,
HideHelpCommand: true,
}

var buildCancelCmd = cli.Command{
Name: "cancel",
Usage: "Cancel a build",
ArgsUsage: "<id>",
Action: handleBuildCancel,
HideHelpCommand: true,
}

func handleBuildList(ctx context.Context, cmd *cli.Command) error {
client := hypeman.NewClient(getDefaultRequestOptions(cmd)...)

var opts []option.RequestOption
if cmd.Root().Bool("debug") {
opts = append(opts, debugMiddlewareOption)
}

format := cmd.Root().String("format")
transform := cmd.Root().String("transform")

if format != "auto" {
var res []byte
opts = append(opts, option.WithResponseBodyInto(&res))
_, err := client.Builds.List(ctx, opts...)
if err != nil {
return err
}
obj := gjson.ParseBytes(res)
return ShowJSON(os.Stdout, "build list", obj, format, transform)
}

builds, err := client.Builds.List(ctx, opts...)
if err != nil {
return err
}

quietMode := cmd.Bool("quiet")

if quietMode {
for _, b := range *builds {
fmt.Println(b.ID)
}
return nil
}

if len(*builds) == 0 {
fmt.Fprintln(os.Stderr, "No builds found.")
return nil
}

table := NewTableWriter(os.Stdout, "ID", "STATUS", "IMAGE", "DURATION", "CREATED")
table.TruncOrder = []int{2, 0, 4} // IMAGE first, then ID, CREATED
for _, b := range *builds {
imageRef := b.ImageRef
if imageRef == "" {
imageRef = "-"
}

duration := "-"
if b.DurationMs > 0 {
secs := b.DurationMs / 1000
if secs < 60 {
duration = fmt.Sprintf("%ds", secs)
} else {
duration = fmt.Sprintf("%dm%ds", secs/60, secs%60)
}
}

table.AddRow(
TruncateID(b.ID),
string(b.Status),
imageRef,
duration,
FormatTimeAgo(b.CreatedAt),
)
}
table.Render()

return nil
}

func handleBuildGet(ctx context.Context, cmd *cli.Command) error {
args := cmd.Args().Slice()
if len(args) < 1 {
return fmt.Errorf("build ID required\nUsage: hypeman build get <id>")
}

id := args[0]

client := hypeman.NewClient(getDefaultRequestOptions(cmd)...)

var opts []option.RequestOption
if cmd.Root().Bool("debug") {
opts = append(opts, debugMiddlewareOption)
}

var res []byte
opts = append(opts, option.WithResponseBodyInto(&res))
_, err := client.Builds.Get(ctx, id, opts...)
if err != nil {
return err
}

format := cmd.Root().String("format")
transform := cmd.Root().String("transform")

obj := gjson.ParseBytes(res)
return ShowJSON(os.Stdout, "build get", obj, format, transform)
}

func handleBuildCancel(ctx context.Context, cmd *cli.Command) error {
args := cmd.Args().Slice()
if len(args) < 1 {
return fmt.Errorf("build ID required\nUsage: hypeman build cancel <id>")
}

id := args[0]

client := hypeman.NewClient(getDefaultRequestOptions(cmd)...)

var opts []option.RequestOption
if cmd.Root().Bool("debug") {
opts = append(opts, debugMiddlewareOption)
}

err := client.Builds.Cancel(ctx, id, opts...)
if err != nil {
return err
}

fmt.Fprintf(os.Stderr, "Cancelled build %s\n", id)
return nil
}
2 changes: 2 additions & 0 deletions pkg/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ func init() {
&startCmd,
&standbyCmd,
&restoreCmd,
&imageCmd,
&ingressCmd,
&volumeCmd,
&resourcesCmd,
&deviceCmd,
{
Expand Down
175 changes: 175 additions & 0 deletions pkg/cmd/imagecmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package cmd

import (
"context"
"fmt"
"net/url"
"os"

"github.com/kernel/hypeman-go"
"github.com/kernel/hypeman-go/option"
"github.com/tidwall/gjson"
"github.com/urfave/cli/v3"
)

var imageCmd = cli.Command{
Name: "image",
Usage: "Manage images",
Commands: []*cli.Command{
&imageListCmd,
&imageGetCmd,
&imageDeleteCmd,
},
HideHelpCommand: true,
}

var imageListCmd = cli.Command{
Name: "list",
Usage: "List images",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "quiet",
Aliases: []string{"q"},
Usage: "Only display image names",
},
},
Action: handleImageList,
HideHelpCommand: true,
}

var imageGetCmd = cli.Command{
Name: "get",
Usage: "Get image details",
ArgsUsage: "<name>",
Action: handleImageGet,
HideHelpCommand: true,
}

var imageDeleteCmd = cli.Command{
Name: "delete",
Aliases: []string{"rm"},
Usage: "Delete an image",
ArgsUsage: "<name>",
Action: handleImageDelete,
HideHelpCommand: true,
}

func handleImageList(ctx context.Context, cmd *cli.Command) error {
client := hypeman.NewClient(getDefaultRequestOptions(cmd)...)

var opts []option.RequestOption
if cmd.Root().Bool("debug") {
opts = append(opts, debugMiddlewareOption)
}

format := cmd.Root().String("format")
transform := cmd.Root().String("transform")

if format != "auto" {
var res []byte
opts = append(opts, option.WithResponseBodyInto(&res))
_, err := client.Images.List(ctx, opts...)
if err != nil {
return err
}
obj := gjson.ParseBytes(res)
return ShowJSON(os.Stdout, "image list", obj, format, transform)
}

images, err := client.Images.List(ctx, opts...)
if err != nil {
return err
}

quietMode := cmd.Bool("quiet")

if quietMode {
for _, img := range *images {
fmt.Println(img.Name)
}
return nil
}

if len(*images) == 0 {
fmt.Fprintln(os.Stderr, "No images found.")
return nil
}

table := NewTableWriter(os.Stdout, "NAME", "STATUS", "DIGEST", "SIZE", "CREATED")
table.TruncOrder = []int{0, 2, 4} // NAME first, then DIGEST, CREATED
for _, img := range *images {
digest := img.Digest
if len(digest) > 19 {
digest = digest[:19]
}

size := "-"
if img.SizeBytes > 0 {
size = formatBytes(img.SizeBytes)
}

table.AddRow(
img.Name,
string(img.Status),
digest,
size,
FormatTimeAgo(img.CreatedAt),
)
}
table.Render()

return nil
}

func handleImageGet(ctx context.Context, cmd *cli.Command) error {
args := cmd.Args().Slice()
if len(args) < 1 {
return fmt.Errorf("image name required\nUsage: hypeman image get <name>")
}

name := args[0]

client := hypeman.NewClient(getDefaultRequestOptions(cmd)...)

var opts []option.RequestOption
if cmd.Root().Bool("debug") {
opts = append(opts, debugMiddlewareOption)
}

var res []byte
opts = append(opts, option.WithResponseBodyInto(&res))
_, err := client.Images.Get(ctx, url.PathEscape(name), opts...)
if err != nil {
return err
}

format := cmd.Root().String("format")
transform := cmd.Root().String("transform")

obj := gjson.ParseBytes(res)
return ShowJSON(os.Stdout, "image get", obj, format, transform)
}

func handleImageDelete(ctx context.Context, cmd *cli.Command) error {
args := cmd.Args().Slice()
if len(args) < 1 {
return fmt.Errorf("image name required\nUsage: hypeman image delete <name>")
}

name := args[0]

client := hypeman.NewClient(getDefaultRequestOptions(cmd)...)

var opts []option.RequestOption
if cmd.Root().Bool("debug") {
opts = append(opts, debugMiddlewareOption)
}

err := client.Images.Delete(ctx, url.PathEscape(name), opts...)
if err != nil {
return err
}

fmt.Fprintf(os.Stderr, "Deleted image %s\n", name)
return nil
}
Loading