Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
6b79bd1
Add cli-redirect endpoint for babylon CLI
fridim Apr 14, 2026
ee22a88
Add babylon CLI
fridim Apr 14, 2026
5da4502
chore: add .worktrees to gitignore
fridim Apr 16, 2026
cfdad82
refactor(cli): extract shared helpers from service_order.go
fridim Apr 16, 2026
bc000de
feat(cli): add workshop parent command
fridim Apr 16, 2026
30a7484
feat(cli): add Workshop and WorkshopProvision types
fridim Apr 16, 2026
c971629
feat(cli): add workshop API functions
fridim Apr 16, 2026
57b805c
feat(cli): add workshop delete command
fridim Apr 16, 2026
d06b59b
feat(cli): add workshop list command
fridim Apr 16, 2026
75416bc
feat(cli): add workshop create command
fridim Apr 16, 2026
11201dd
fix(cli): include workshop name in provision failure error
fridim Apr 16, 2026
6c809d6
feat(cli): add workshop status command
fridim Apr 16, 2026
b9f2190
feat(cli): add --watch flag to workshop status
fridim Apr 16, 2026
56e2e0c
feat(cli): show provisioning timestamps from AnarchySubject towerJobs
fridim Apr 16, 2026
a330f29
fix(cli): always serialize bool fields in WorkshopSpec
fridim Apr 16, 2026
7c67c10
fix: add default: false to Workshop CRD boolean fields
fridim Apr 16, 2026
5fb99e4
Revert "fix: add default: false to Workshop CRD boolean fields"
fridim Apr 16, 2026
735a03e
feat(cli): add cross-compilation support and version command
fridim Apr 28, 2026
f5d031c
feat(cli): enrich service status with provision data and tower jobs
fridim Apr 29, 2026
9d276e6
feat(cli): add 'services' alias for service command
fridim Apr 29, 2026
27869da
feat(cli): clarify login help text and --server flag description
fridim Apr 29, 2026
b1c6df2
fix(cli): skip 403 namespaces when searching catalog items
fridim Apr 29, 2026
2198c9a
feat(cli): show per-request timing in --debug output
fridim Apr 29, 2026
4598c9a
fix(cli): only store OAuth proxy cookie during login
fridim Apr 29, 2026
367284b
feat(cli): show tower job URLs in service status
fridim Apr 29, 2026
762de47
fix(cli): prepend https:// to tower job URLs when missing
fridim Apr 29, 2026
7803397
fix(cli): reorder service status output for readability
fridim Apr 29, 2026
9160762
feat(cli): use tree connectors for tower jobs in service status
fridim Apr 29, 2026
763c9cb
feat(cli): show human-readable duration for lifespan end
fridim Apr 29, 2026
c8c9f81
Add cli/build to .gitignore
fridim Apr 29, 2026
8793adf
fix(cli): catalog describe searches catalog namespaces, not user name…
fridim Apr 29, 2026
1225a3a
feat(cli): add GitHub Action for cross-platform release builds
fridim Apr 29, 2026
b564dae
ci: temporarily trigger cli build on cli branch push
fridim Apr 29, 2026
eb2f7bd
Add gh suggestion
fridim Apr 29, 2026
540280c
ci: bump GitHub Actions to Node.js 24 compatible versions
fridim Apr 29, 2026
6594594
fix(cli): preserve HTTP method on redirects
fridim May 22, 2026
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
77 changes: 77 additions & 0 deletions .github/workflows/cli-release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
name: cli-release
on:
push:
branches:
- cli # temporary: remove after testing
tags:
- "cli-v[0-9]*"
workflow_dispatch:
inputs:
version:
description: "Version (e.g. v0.1.0)"
required: true
type: string

permissions:
contents: read

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- goos: linux
goarch: amd64
- goos: linux
goarch: arm64
- goos: darwin
goarch: amd64
- goos: darwin
goarch: arm64
- goos: windows
goarch: amd64

steps:
- name: Checkout
uses: actions/checkout@v5

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: cli/go.mod

- name: Get version
id: version
run: |
if [[ -n "${{ github.event.inputs.version }}" ]]; then
echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
else
echo "VERSION=${GITHUB_REF#refs/tags/cli-}" >> $GITHUB_OUTPUT
fi

- name: Build
working-directory: cli
env:
CGO_ENABLED: "0"
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: |
EXT=""
if [[ "${{ matrix.goos }}" == "windows" ]]; then EXT=".exe"; fi
OUTPUT="babylon-${{ matrix.goos }}-${{ matrix.goarch }}${EXT}"
go build \
-ldflags="-X 'main.version=${{ steps.version.outputs.VERSION }}' -X 'main.buildCommit=${{ github.sha }}' -X 'main.buildTime=$(date -u)'" \
-o "../build/${OUTPUT}" .
cd ../build && sha256sum "${OUTPUT}" > "${OUTPUT}.sha256"

- name: Upload artifact
uses: actions/upload-artifact@v5
with:
name: babylon-${{ matrix.goos }}-${{ matrix.goarch }}
path: build/babylon-*
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ notifier/values.yaml
*.swp
__pycache__
.vscode
.worktrees
/cli/build
42 changes: 42 additions & 0 deletions catalog/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,48 @@ async def get_auth_session(request):
finally:
await api_client.close()

@routes.get('/auth/cli-redirect')
async def get_auth_cli_redirect(request):
"""CLI login endpoint. Called after OAuth flow completes.

The browser has the _oauth2_proxy cookie at this point. This endpoint
creates a session (via the normal OAuth proxy headers) and redirects
to the CLI's localhost callback, POSTing the session token and proxy
cookie so the CLI can use both for subsequent API calls.
"""
callback = request.query.get('callback', '')
if not callback or not callback.startswith('http://localhost'):
raise web.HTTPBadRequest(reason="Missing or invalid 'callback' parameter (must be http://localhost)")

user = await get_proxy_user(request)
user_groups = await get_user_groups(user)
api_client, session, token = await start_user_session(user, user_groups)
try:
user_name = user['metadata']['name']

# Collect all OAuth proxy cookies to forward to the CLI.
# The cookie name is dynamic (based on hostname), so we send all of them.
import html as html_module
cookie_fields = ""
for name, value in request.cookies.items():
cookie_fields += f' <input type="hidden" name="cookie_{html_module.escape(name)}" value="{html_module.escape(value)}">\n'

html = f"""<!DOCTYPE html>
<html>
<head><title>Babylon CLI Login</title></head>
<body>
<p>Logging in as <strong>{html_module.escape(user_name)}</strong>... you can close this tab.</p>
<form id="f" method="POST" action="{html_module.escape(callback)}">
<input type="hidden" name="token" value="{html_module.escape(token)}">
<input type="hidden" name="user" value="{html_module.escape(user_name)}">
{cookie_fields}</form>
<script>document.getElementById('f').submit();</script>
</body>
</html>"""
return web.Response(text=html, content_type='text/html')
finally:
await api_client.close()

@routes.get("/auth/users/{user_name}")
async def get_auth_users_info(request):
user_name = request.match_info.get('user_name')
Expand Down
33 changes: 33 additions & 0 deletions cli/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
SHELL = /bin/sh
VERSION ?= $(shell git describe --tags 2>/dev/null || echo development)
COMMIT ?= $(shell git rev-parse HEAD 2>/dev/null)
DATE ?= $(shell date -u)
export CGO_ENABLED=0

CLI_LDFLAGS = -X 'main.version=$(VERSION)' -X 'main.buildCommit=$(COMMIT)' -X 'main.buildTime=$(DATE)'
CLI_PLATFORMS ?= linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64

babylon:
go build -ldflags="$(CLI_LDFLAGS)" -o build/babylon .

babylon-cross:
@mkdir -p build
@for platform in $(CLI_PLATFORMS); do \
os=$${platform%/*}; \
arch=$${platform#*/}; \
output=build/babylon-$${os}-$${arch}; \
if [ "$$os" = "windows" ]; then output=$${output}.exe; fi; \
echo "Building $$output ..."; \
GOOS=$$os GOARCH=$$arch go build -ldflags="$(CLI_LDFLAGS)" -o $$output . || exit 1; \
done
@echo "Done. Binaries:"
@ls -lh build/babylon-*
@echo sha256sums:
@echo '```'
@cd build && sha256sum babylon-*
@echo '```'

clean:
rm -f build/babylon*

.PHONY: babylon babylon-cross clean
13 changes: 13 additions & 0 deletions cli/cmd/catalog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package cmd

import "github.com/spf13/cobra"

var catalogCmd = &cobra.Command{
Use: "catalog",
Short: "Browse and inspect the service catalog",
Long: "Commands for listing and describing available catalog items.",
}

func init() {
rootCmd.AddCommand(catalogCmd)
}
136 changes: 136 additions & 0 deletions cli/cmd/catalog_describe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package cmd

import (
"fmt"
"io"
"strings"

"github.com/spf13/cobra"

"github.com/redhat-gpte/babylon/cli/pkg/output"
"github.com/redhat-gpte/babylon/cli/pkg/types"
)

var catalogDescribeCmd = &cobra.Command{
Use: "describe <catalog-item-name>",
Short: "Show details of a catalog item",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]

var ci *types.CatalogItem
var err error

// Only use explicit -n flag for catalog lookups, not the default
// service namespace (which would always 403).
if cmd.Flags().Changed("namespace") {
ci, err = apiClient.GetCatalogItem(namespace, name)
} else {
ci, err = apiClient.FindCatalogItem(name)
}
if err != nil {
return err
}

return output.Print(getOutputFormat(), ci, func(w io.Writer) {
printCatalogItemDetails(w, ci)
})
},
}

func printCatalogItemDetails(w io.Writer, ci *types.CatalogItem) {
fmt.Fprintf(w, "Name: %s\n", ci.Metadata.Name)
fmt.Fprintf(w, "Namespace: %s\n", ci.Metadata.Namespace)
if ci.Spec.DisplayName != "" {
fmt.Fprintf(w, "Display: %s\n", ci.Spec.DisplayName)
}
if ci.Spec.Category != "" {
fmt.Fprintf(w, "Category: %s\n", ci.Spec.Category)
}

if ci.Spec.Description != nil && ci.Spec.Description.Content != "" {
desc := stripTags(ci.Spec.Description.Content)
if len(desc) > 200 {
desc = desc[:200] + "..."
}
fmt.Fprintf(w, "Description: %s\n", desc)
}

if ci.Spec.ProvisionTimeEstimate != "" {
fmt.Fprintf(w, "Provision: ~%s\n", ci.Spec.ProvisionTimeEstimate)
}

if ci.Spec.Lifespan != nil {
parts := []string{}
if ci.Spec.Lifespan.Default != "" {
parts = append(parts, "default="+ci.Spec.Lifespan.Default)
}
if ci.Spec.Lifespan.Maximum != "" {
parts = append(parts, "max="+ci.Spec.Lifespan.Maximum)
}
if len(parts) > 0 {
fmt.Fprintf(w, "Lifespan: %s\n", strings.Join(parts, ", "))
}
}

if ci.Spec.Runtime != nil {
parts := []string{}
if ci.Spec.Runtime.Default != "" {
parts = append(parts, "default="+ci.Spec.Runtime.Default)
}
if ci.Spec.Runtime.Maximum != "" {
parts = append(parts, "max="+ci.Spec.Runtime.Maximum)
}
if len(parts) > 0 {
fmt.Fprintf(w, "Runtime: %s\n", strings.Join(parts, ", "))
}
}

if len(ci.Spec.Keywords) > 0 {
fmt.Fprintf(w, "Keywords: %s\n", strings.Join(ci.Spec.Keywords, ", "))
}

if ci.Status != nil && ci.Status.Rating > 0 {
fmt.Fprintf(w, "Rating: %.1f/5\n", ci.Status.Rating)
}

if len(ci.Spec.Parameters) > 0 {
fmt.Fprintf(w, "\nParameters:\n")
pt := output.NewTable("NAME", "LABEL", "REQUIRED", "DESCRIPTION")
for _, p := range ci.Spec.Parameters {
label := p.FormLabel
if label == "" {
label = p.Name
}
req := ""
if p.Required {
req = "yes"
}
pt.AddRow(p.Name, label, req, p.Description)
}
pt.Render(w)
}
}

func stripTags(s string) string {
var result strings.Builder
inTag := false
for _, r := range s {
if r == '<' {
inTag = true
continue
}
if r == '>' {
inTag = false
continue
}
if !inTag {
result.WriteRune(r)
}
}
return result.String()
}

func init() {
catalogCmd.AddCommand(catalogDescribeCmd)
}
68 changes: 68 additions & 0 deletions cli/cmd/catalog_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package cmd

import (
"fmt"
"io"
"strings"

"github.com/spf13/cobra"

"github.com/redhat-gpte/babylon/cli/pkg/output"
"github.com/redhat-gpte/babylon/cli/pkg/types"
)

var catalogListCategory string

var catalogListCmd = &cobra.Command{
Use: "list",
Short: "List available catalog items",
RunE: func(cmd *cobra.Command, args []string) error {
namespaces := apiClient.CatalogNamespaces()
if cmd.Flags().Changed("namespace") {
namespaces = []string{namespace}
}

items, err := apiClient.ListAllCatalogItems(namespaces)
if err != nil {
return err
}

// Filter by category if specified
if catalogListCategory != "" {
filtered := make([]types.CatalogItem, 0)
for _, item := range items {
if strings.EqualFold(item.Spec.Category, catalogListCategory) {
filtered = append(filtered, item)
}
}
items = filtered
}

return output.Print(getOutputFormat(), items, func(w io.Writer) {
if len(items) == 0 {
output.PrintMessage(w, "No catalog items found.")
return
}
t := output.NewTable("NAME", "DISPLAY NAME", "CATEGORY", "NAMESPACE")
for _, item := range items {
displayName := item.Spec.DisplayName
if displayName == "" {
displayName = item.Metadata.Name
}
t.AddRow(
item.Metadata.Name,
displayName,
item.Spec.Category,
item.Metadata.Namespace,
)
}
t.Render(w)
fmt.Fprintf(w, "\n%d catalog item(s)\n", len(items))
})
},
}

func init() {
catalogCmd.AddCommand(catalogListCmd)
catalogListCmd.Flags().StringVar(&catalogListCategory, "category", "", "filter by category")
}
Loading
Loading