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
37 changes: 30 additions & 7 deletions cmd/caib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ bin/caib build <manifest.aib.yml> [flags]
| `-D`, `--define` | | Custom definition `KEY=VALUE` (repeatable) |
| `--timeout` | `60` | Timeout in minutes |
| `-w`, `--wait` | `false` | Wait for build to complete |
| `-f`, `--follow` | `false` | Follow build logs |
| `-f`, `--follow` | `true` | Follow build logs |
| `--internal-registry` | `false` | Push to OpenShift internal registry (no credentials needed) |
| `--image-name` | (build name) | Override image name in internal registry |
| `--image-tag` | (build name) | Override tag in internal registry |
Expand Down Expand Up @@ -177,7 +177,7 @@ bin/caib disk <container-ref> [flags]
| `--storage-class` | | Kubernetes storage class |
| `--timeout` | `60` | Timeout in minutes |
| `-w`, `--wait` | `false` | Wait for build to complete |
| `-f`, `--follow` | `false` | Follow build logs |
| `-f`, `--follow` | `true` | Follow build logs |

**Examples:**

Expand Down Expand Up @@ -225,7 +225,7 @@ bin/caib build-dev <manifest.aib.yml> [flags]
| `-D`, `--define` | | Custom definition `KEY=VALUE` (repeatable) |
| `--timeout` | `60` | Timeout in minutes |
| `-w`, `--wait` | `false` | Wait for build to complete |
| `-f`, `--follow` | `false` | Follow build logs |
| `-f`, `--follow` | `true` | Follow build logs |

**Examples:**

Expand All @@ -252,16 +252,14 @@ bin/caib build-dev my-manifest.aib.yml \
Downloads artifacts from a completed build.

```bash
bin/caib download --name <build-name> [flags]
bin/caib download <build-name> [flags]
```

| Flag | Default | Description |
|------|---------|-------------|
| `--name` | (required) | Build name |
| `--server` | `$CAIB_SERVER` | Build API server URL |
| `--token` | `$CAIB_TOKEN` | Bearer token |
| `--output-dir` | `./output` | Directory to save artifacts |
| `--compress` | `true` | Keep directory artifacts compressed |
| `-o`, `--output` | (required) | Destination file or directory for downloaded artifact |

### list

Expand All @@ -276,6 +274,31 @@ bin/caib list [flags]
| `--server` | `$CAIB_SERVER` | Build API server URL |
| `--token` | `$CAIB_TOKEN` | Bearer token |

### show

Shows detailed information for a single build, including current status and resolved build parameters.

```bash
bin/caib show <build-name> [flags]
```

| Flag | Default | Description |
|------|---------|-------------|
| `--server` | `$CAIB_SERVER` | Build API server URL |
| `--token` | `$CAIB_TOKEN` | Bearer token |
| `-o`, `--output` | `table` | Output format: `table`, `json`, `yaml` |

**Examples:**

```bash
# Human-friendly detail view
bin/caib show my-build

# Machine-readable output
bin/caib show my-build -o json
bin/caib show my-build -o yaml
```

## Bootc vs Dev Builds

| Aspect | `build` (bootc) | `build-dev` |
Expand Down
178 changes: 177 additions & 1 deletion cmd/caib/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ var (
serverURL string
manifest string
buildName string
showOutputFormat string
distro string
target string
architecture string
Expand Down Expand Up @@ -469,6 +470,21 @@ Examples:
Run: runList,
}

showCmd := &cobra.Command{
Use: "show <build-name>",
Short: "Show detailed information for an ImageBuild",
Long: `Show retrieves detailed status and output fields for a single ImageBuild.

Examples:
# Show details in table format
caib show my-build

# Show details as JSON
caib show my-build -o json`,
Args: cobra.ExactArgs(1),
Run: runShow,
}

downloadCmd := &cobra.Command{
Use: "download <build-name>",
Short: "Download disk image artifact from a completed build",
Expand Down Expand Up @@ -544,6 +560,16 @@ Example:
&authToken, "token", os.Getenv("CAIB_TOKEN"),
"Bearer token for authentication (e.g., OpenShift access token)",
)
showCmd.Flags().StringVar(
&serverURL, "server", config.DefaultServer(), "REST API server base URL (e.g. https://api.example)",
)
showCmd.Flags().StringVar(
&authToken, "token", os.Getenv("CAIB_TOKEN"),
"Bearer token for authentication (e.g., OpenShift access token)",
)
showCmd.Flags().StringVarP(
&showOutputFormat, "output", "o", "table", "Output format (table, json, yaml)",
)

// disk command flags (create disk from existing container)
diskCmd.Flags().StringVar(&serverURL, "server", config.DefaultServer(), "REST API server base URL")
Expand Down Expand Up @@ -625,7 +651,7 @@ Example:
_ = flashCmd.MarkFlagRequired("client")

// Add all commands
rootCmd.AddCommand(buildCmd, diskCmd, buildDevCmd, listCmd, downloadCmd, flashCmd, loginCmd, catalog.NewCatalogCmd())
rootCmd.AddCommand(buildCmd, diskCmd, buildDevCmd, listCmd, showCmd, downloadCmd, flashCmd, loginCmd, catalog.NewCatalogCmd())
// Add deprecated aliases for backwards compatibility
rootCmd.AddCommand(buildBootcAliasCmd, buildLegacyAliasCmd, buildTraditionalAliasCmd)

Expand Down Expand Up @@ -1902,6 +1928,156 @@ func runList(_ *cobra.Command, _ []string) {
}
}

func runShow(_ *cobra.Command, args []string) {
ctx := context.Background()
showBuildName := args[0]

if strings.TrimSpace(serverURL) == "" {
handleError(fmt.Errorf("--server is required (or set CAIB_SERVER, or run 'caib login <server-url>')"))
}

var st *buildapitypes.BuildResponse
err := executeWithReauth(serverURL, &authToken, func(api *buildapiclient.Client) error {
var err error
st, err = api.GetBuild(ctx, showBuildName)
return err
})
if err != nil {
handleError(fmt.Errorf("error getting ImageBuild %s: %w", showBuildName, err))
}

// Backward-compatible fallback for older API servers that do not yet include response parameters.
if st.Parameters == nil {
_ = executeWithReauth(serverURL, &authToken, func(api *buildapiclient.Client) error {
tpl, err := api.GetBuildTemplate(ctx, showBuildName)
if err != nil {
return err
}
st.Parameters = buildParametersFromTemplate(tpl)
return nil
})
}
Comment on lines +1949 to +1959

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Silently swallowing template fallback errors hides legitimate failures.

When Parameters is nil (older server), the template fetch error is discarded with _ = executeWithReauth(...). If the failure is due to a network error, auth issue, or server bug (not just a missing endpoint), the user sees no parameters with no explanation.

Consider logging a warning to stderr on failure:

Suggested improvement
 	if st.Parameters == nil {
-		_ = executeWithReauth(serverURL, &authToken, func(api *buildapiclient.Client) error {
+		err := executeWithReauth(serverURL, &authToken, func(api *buildapiclient.Client) error {
 			tpl, err := api.GetBuildTemplate(ctx, showBuildName)
 			if err != nil {
 				return err
 			}
 			st.Parameters = buildParametersFromTemplate(tpl)
 			return nil
 		})
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "Warning: could not fetch build parameters: %v\n", err)
+		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Backward-compatible fallback for older API servers that do not yet include response parameters.
if st.Parameters == nil {
_ = executeWithReauth(serverURL, &authToken, func(api *buildapiclient.Client) error {
tpl, err := api.GetBuildTemplate(ctx, showBuildName)
if err != nil {
return err
}
st.Parameters = buildParametersFromTemplate(tpl)
return nil
})
}
// Backward-compatible fallback for older API servers that do not yet include response parameters.
if st.Parameters == nil {
err := executeWithReauth(serverURL, &authToken, func(api *buildapiclient.Client) error {
tpl, err := api.GetBuildTemplate(ctx, showBuildName)
if err != nil {
return err
}
st.Parameters = buildParametersFromTemplate(tpl)
return nil
})
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: could not fetch build parameters: %v\n", err)
}
}
🤖 Prompt for AI Agents
In `@cmd/caib/main.go` around lines 1949 - 1959, The current fallback call to
executeWithReauth ignores errors and swallows failures when st.Parameters is
nil; update the block so you capture the returned error from
executeWithReauth(...) and, if non-nil, write a clear warning to stderr that
includes context (e.g., "failed to fetch build template for showBuildName" or
similar) and the actual error value rather than discarding it; keep the existing
behavior of setting st.Parameters = buildParametersFromTemplate(tpl) on success
and only log (not fatal) on failure so users see why parameters are missing.


switch strings.ToLower(showOutputFormat) {
case "json":
out, err := json.MarshalIndent(st, "", " ")
if err != nil {
handleError(fmt.Errorf("error rendering JSON output: %w", err))
}
fmt.Println(string(out))
case "yaml", "yml":
out, err := yaml.Marshal(st)
if err != nil {
handleError(fmt.Errorf("error rendering YAML output: %w", err))
}
fmt.Print(string(out))
case "table":
printBuildDetails(st)
default:
handleError(fmt.Errorf("invalid output format %q (supported: table, json, yaml)", showOutputFormat))
}
}

func buildParametersFromTemplate(tpl *buildapitypes.BuildTemplateResponse) *buildapitypes.BuildParameters {
if tpl == nil {
return nil
}

params := &buildapitypes.BuildParameters{
Architecture: string(tpl.Architecture),
Distro: string(tpl.Distro),
Target: string(tpl.Target),
Mode: string(tpl.Mode),
ExportFormat: string(tpl.ExportFormat),
Compression: tpl.Compression,
StorageClass: tpl.StorageClass,
AutomotiveImageBuilder: tpl.AutomotiveImageBuilder,
BuilderImage: tpl.BuilderImage,
ContainerRef: tpl.ContainerRef,
BuildDiskImage: tpl.BuildDiskImage,
FlashEnabled: tpl.FlashEnabled,
FlashLeaseDuration: tpl.FlashLeaseDuration,
UseServiceAccountAuth: tpl.UseInternalRegistry,
}

if strings.TrimSpace(params.Architecture) == "" &&
strings.TrimSpace(params.Distro) == "" &&
strings.TrimSpace(params.Target) == "" &&
strings.TrimSpace(params.Mode) == "" &&
strings.TrimSpace(params.ExportFormat) == "" &&
strings.TrimSpace(params.Compression) == "" &&
strings.TrimSpace(params.StorageClass) == "" &&
strings.TrimSpace(params.AutomotiveImageBuilder) == "" &&
strings.TrimSpace(params.BuilderImage) == "" &&
strings.TrimSpace(params.ContainerRef) == "" &&
strings.TrimSpace(params.FlashLeaseDuration) == "" &&
!params.BuildDiskImage &&
!params.FlashEnabled &&
!params.UseServiceAccountAuth {
return nil
}

return params
}

func printBuildDetails(st *buildapitypes.BuildResponse) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
defer func() {
if err := w.Flush(); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to flush output: %v\n", err)
}
}()

rows := [][2]string{
{"Name", st.Name},
{"Phase", st.Phase},
{"Message", st.Message},
{"Requested By", valueOrDash(st.RequestedBy)},
{"Start Time", valueOrDash(st.StartTime)},
{"Completion Time", valueOrDash(st.CompletionTime)},
{"Container Image", valueOrDash(st.ContainerImage)},
{"Disk Image", valueOrDash(st.DiskImage)},
{"Warning", valueOrDash(st.Warning)},
}

if st.Parameters != nil {
rows = append(rows,
[2]string{"Architecture", valueOrDash(st.Parameters.Architecture)},
[2]string{"Distro", valueOrDash(st.Parameters.Distro)},
[2]string{"Target", valueOrDash(st.Parameters.Target)},
[2]string{"Mode", valueOrDash(st.Parameters.Mode)},
[2]string{"Export Format", valueOrDash(st.Parameters.ExportFormat)},
[2]string{"Compression", valueOrDash(st.Parameters.Compression)},
[2]string{"Storage Class", valueOrDash(st.Parameters.StorageClass)},
[2]string{"AIB Image", valueOrDash(st.Parameters.AutomotiveImageBuilder)},
[2]string{"Builder Image", valueOrDash(st.Parameters.BuilderImage)},
)
}

if st.Jumpstarter != nil {
rows = append(rows,
[2]string{"Jumpstarter Available", fmt.Sprintf("%t", st.Jumpstarter.Available)},
[2]string{"Jumpstarter Exporter", valueOrDash(st.Jumpstarter.ExporterSelector)},
[2]string{"Jumpstarter Flash Cmd", valueOrDash(st.Jumpstarter.FlashCmd)},
[2]string{"Jumpstarter Lease ID", valueOrDash(st.Jumpstarter.LeaseID)},
)
}

for _, row := range rows {
if _, err := fmt.Fprintf(w, "%s\t%s\n", row[0], row[1]); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to write output row: %v\n", err)
return
}
}
}

func valueOrDash(v string) string {
if strings.TrimSpace(v) == "" {
return "-"
}
return v
}

func formatAge(rfcTime string) string {
t, err := time.Parse(time.RFC3339, rfcTime)
if err != nil {
Expand Down
42 changes: 29 additions & 13 deletions internal/buildapi/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,9 @@ func (c *Client) GetBuild(ctx context.Context, name string) (*buildapi.BuildResp
return &out, nil
}

// ListBuilds retrieves a list of all builds from the API server.
func (c *Client) ListBuilds(ctx context.Context) ([]buildapi.BuildListItem, error) {
endpoint := c.resolve("/v1/builds")
// GetBuildTemplate retrieves a build template reconstructed from ImageBuild inputs.
func (c *Client) GetBuildTemplate(ctx context.Context, name string) (*buildapi.BuildTemplateResponse, error) {
endpoint := c.resolve(path.Join("/v1/builds", url.PathEscape(name), "template"))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
Expand All @@ -188,12 +188,21 @@ func (c *Client) ListBuilds(ctx context.Context) ([]buildapi.BuildListItem, erro
}()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
return nil, fmt.Errorf("list builds failed: %s: %s", resp.Status, string(b))
return nil, fmt.Errorf("get build template failed: %s: %s", resp.Status, string(b))
}
var out []buildapi.BuildListItem
var out buildapi.BuildTemplateResponse
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, err
}
return &out, nil
}

// ListBuilds retrieves a list of all builds from the API server.
func (c *Client) ListBuilds(ctx context.Context) ([]buildapi.BuildListItem, error) {
var out []buildapi.BuildListItem
if err := c.listJSON(ctx, c.resolve("/v1/builds"), "list builds", &out); err != nil {
return nil, err
}
return out, nil
}

Expand Down Expand Up @@ -277,17 +286,25 @@ func (c *Client) GetFlash(ctx context.Context, name string) (*buildapi.FlashResp

// ListFlash retrieves a list of all flash jobs from the API server.
func (c *Client) ListFlash(ctx context.Context) ([]buildapi.FlashListItem, error) {
endpoint := c.resolve("/v1/flash")
var out []buildapi.FlashListItem
if err := c.listJSON(ctx, c.resolve("/v1/flash"), "list flash", &out); err != nil {
return nil, err
}
return out, nil
}

// listJSON performs a list-style GET request and decodes a JSON array response into out.
func (c *Client) listJSON(ctx context.Context, endpoint, operation string, out any) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
return err
}
if c.authToken != "" {
req.Header.Set("Authorization", "Bearer "+c.authToken)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
return err
}
defer func() {
if err := resp.Body.Close(); err != nil {
Expand All @@ -296,13 +313,12 @@ func (c *Client) ListFlash(ctx context.Context) ([]buildapi.FlashListItem, error
}()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
return nil, fmt.Errorf("list flash failed: %s: %s", resp.Status, string(b))
return fmt.Errorf("%s failed: %s: %s", operation, resp.Status, string(b))
}
var out []buildapi.FlashListItem
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, err
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
return err
}
return out, nil
return nil
}

// Upload represents a file to upload to the build API.
Expand Down
16 changes: 16 additions & 0 deletions internal/buildapi/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -1596,6 +1596,22 @@ func (a *APIServer) getBuild(c *gin.Context, name string) {
RegistryToken: registryToken,
Warning: warning,
Jumpstarter: jumpstarterInfo,
Parameters: &BuildParameters{
Architecture: build.Spec.Architecture,
Distro: build.Spec.GetDistro(),
Target: build.Spec.GetTarget(),
Mode: build.Spec.GetMode(),
ExportFormat: build.Spec.GetExportFormat(),
Compression: build.Spec.GetCompression(),
StorageClass: build.Spec.StorageClass,
AutomotiveImageBuilder: build.Spec.GetAIBImage(),
BuilderImage: build.Spec.GetBuilderImage(),
ContainerRef: build.Spec.GetContainerRef(),
BuildDiskImage: build.Spec.GetBuildDiskImage(),
FlashEnabled: build.Spec.IsFlashEnabled(),
FlashLeaseDuration: build.Spec.GetFlashLeaseDuration(),
UseServiceAccountAuth: build.Spec.GetUseServiceAccountAuth(),
},
})
}

Expand Down
Loading