Skip to content
Open
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
5 changes: 5 additions & 0 deletions internal/frontend/http/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ func NewTestFrontend(logger *zap.Logger) *Frontend {
func (f *Frontend) WrapHandler(h Handler) httprouter.Handle {
return f.wrapper(h)
}

// HandleLLMsTxt exposes the llms.txt handler for external tests.
func (f *Frontend) HandleLLMsTxt() Handler {
return f.handleLLMsTxt
}
3 changes: 3 additions & 0 deletions internal/frontend/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ func NewFrontend(
registerPublicRoute(frontend.router.HEAD, "/talosctl/:version/:path", frontend.handleTalosctl)
registerPublicRoute(frontend.router.GET, "/talosctl/:version/:path", frontend.handleTalosctl)

// llms.txt - public
registerPublicRoute(frontend.router.GET, "/llms.txt", frontend.handleLLMsTxt)

// UI - require auth (consistent with all other schematic-creating endpoints)
registerRoute(frontend.router.GET, "/", frontend.handleUI)
registerRoute(frontend.router.HEAD, "/", frontend.handleUI)
Expand Down
186 changes: 186 additions & 0 deletions internal/frontend/http/llms.txt

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we render this on build? This is technically just a docs/API.md with some minor changes, right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, it's just a raw text endpoint. I'm not sure what the difference would be with rendering on build vs embedding. Happy to do it a different way, I just wanted something that was easier for discovery and usage.

Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# Talos Linux Image Factory

The Image Factory builds and serves customized Talos Linux boot artifacts. Artifacts are parameterized by a **schematic** (a hash-identified YAML document describing customizations) and a **Talos Linux version**. The official instance is at `https://factory.talos.dev` (HTTP/OCI) and `https://pxe.talos.dev` (PXE).

## Core Concepts

**Schematic**: A YAML document that describes image customizations. Submitting one returns a stable SHA-256 ID. The same customizations always produce the same ID. The well-known default schematic (no customizations) has ID `376567988ad370138ad8b2698212367b8edcb69b5fd68c80be1f2ec7d603b4ba`.

**Version**: A Talos Linux release version string, e.g. `v1.5.0`. Use `GET /versions` to list available versions.

**Model**: The combination of a schematic ID and a Talos version. From a model you can derive any artifact type.

## Typical Workflow

1. POST a schematic → get back an ID
2. GET `/versions` to find a version to target
3. GET `/image/:schematic/:version/:path` to download an artifact

## HTTP API (`https://factory.talos.dev`)

**Authentication**: The official public instance (`factory.talos.dev`) does not require authentication. Self-hosted deployments may enable an auth provider, in which case all schematic and image endpoints require credentials (HTTP Basic Auth via htpasswd). If you receive a `401 Unauthorized`, check the deployment's authentication configuration. The `/versions`, `/version/:version/extensions/official`, `/version/:version/overlays/official`, `/secureboot/signing-cert.pem`, and `/oci/cosign/signing-key.pub` endpoints are typically public even when auth is enabled.

### POST /schematics

Create a schematic. Request body is YAML (or JSON). Returns `{"id": "<sha256>", "schematic": "<canonical-yaml>"}`.

```yaml
customization:
extraKernelArgs: # optional
- vga=791
meta: # optional, initial Talos META
- key: 0xa
value: "{}"
systemExtensions:
officialExtensions: # optional
- siderolabs/gvisor
- siderolabs/amd-ucode
secureboot: # optional, SecureBoot images only
includeWellKnownCertificates: true
enrollKeys: force # off | manual | if-safe | force
bootloader: sd-boot # optional: auto | sd-boot | dual-boot | grub
embeddedMachineConfiguration: | # optional, YAML machine config docs
apiVersion: v1alpha1
kind: HostnameConfig
hostname: my-host
diskImage: # optional, disk images only
sectorSize: 4096
overlay: # optional, for SBC/overlay targets
image: ghcr.io/siderolabs/sbc-raspberry-pi
name: rpi_generic
options:
data: "mydata"
```

### GET /schematics/:schematic

Retrieve schematic YAML by ID. Returns 404 if not found.

### GET /versions

List available Talos Linux versions. Returns a JSON array of version strings.

```json
["v1.5.0","v1.5.1","v1.5.2"]
```

### GET /version/:version/extensions/official

List official system extensions for a version. Returns array of `{name, ref, digest}`.

### GET /version/:version/overlays/official

List official overlays (e.g. SBC support) for a version. Returns array of `{name, image, ref, digest}`.

### GET /image/:schematic/:version/:path

Download a boot artifact. `:path` values:

| Path pattern | Description |
|---|---|
| `kernel-<arch>` | Raw kernel (e.g. `kernel-amd64`) |
| `cmdline-<platform>-<arch>[-secureboot]` | Kernel command line |
| `initramfs-<arch>.xz` | Initramfs (with extensions) |
| `<platform>-<arch>[-secureboot].iso` | ISO image |
| `<platform>-<arch>[-secureboot]-uki.efi` | UEFI UKI image |
| `installer-<arch>[-secureboot].tar` | Metal installer OCI tar |
| `<platform>-installer-<arch>[-secureboot].tar` | Platform-specific installer |
| `metal-<arch>[-secureboot].raw.xz` | Raw disk image (metal) |
| `aws-<arch>.raw.xz` | Raw disk image for AWS AMI import |
| `gcp-<arch>.raw.tar.gz` | Raw disk image for GCE import |

`<arch>` is `amd64` or `arm64`. `<platform>` examples: `metal`, `aws`, `gcp`, `azure`, `vmware`.

Append `.sha256` or `.sha512` to any path to get a checksum file (Enterprise only).

### GET /talosctl/:version

List `talosctl` binary download URLs for a version (available from v1.11.0+).

### GET /talosctl/:version/:path

Download a `talosctl` binary. `:path` example: `talosctl-linux-amd64`.

### GET /secureboot/signing-cert.pem

Returns the PEM-encoded SecureBoot signing certificate for manual UEFI enrollment.

## PXE API (`https://pxe.talos.dev`)

### GET /pxe/:schematic/:version/:path

Returns an iPXE script that boots Talos Linux. `:path` is `<platform>-<arch>[-secureboot]`, e.g. `metal-amd64`.

Example iPXE chain URL to embed in firmware:
```
chain --replace --autofree https://pxe.talos.dev/pxe/376567988ad370138ad8b2698212367b8edcb69b5fd68c80be1f2ec7d603b4ba/v1.5.0/metal-${buildarch}
```

## OCI Registry API (`factory.talos.dev`)

Pull installer images directly with `docker`/`crane`/`skopeo`:

```
# Legacy form
docker pull factory.talos.dev/installer/<schematic>:<version>
docker pull factory.talos.dev/installer-secureboot/<schematic>:<version>

# Current form (preferred)
docker pull factory.talos.dev/metal-installer/<schematic>:<version>
docker pull factory.talos.dev/aws-installer/<schematic>:<version>

# latest tag resolves to latest stable (non-prerelease) version
docker pull factory.talos.dev/metal-installer/<schematic>:latest
```

### GET /oci/cosign/signing-key.pub

Returns the PEM-encoded public key used to sign installer images. Verify with:

```shell
cosign verify --offline --insecure-ignore-tlog --insecure-ignore-sct \
--key signing-key.pub factory.talos.dev/metal-installer/<schematic>:<version>
```

## Enterprise-Only APIs

Requires Talos Enterprise Image Factory. Available from Talos v1.11.0+.

### GET /spdx/:schematic/:version/:arch

Returns an SPDX 2.3 JSON SBOM for the given schematic and version.

### GET /vex/:version/vex.json

Returns a VEX JSON document for vulnerability suppression.

### GET /scans/:schematic/:version/:arch/:report

Returns a vulnerability scan report. `:report` suffix: `.json`, `.table`, `.sarif`, `.cdx`.

## Examples

Download a default metal AMD64 ISO for Talos v1.7.0:
```shell
curl -LO https://factory.talos.dev/image/376567988ad370138ad8b2698212367b8edcb69b5fd68c80be1f2ec7d603b4ba/v1.7.0/metal-amd64.iso
```

Create a schematic with an extension and download its installer image:
```shell
# 1. Create schematic
curl -s -X POST https://factory.talos.dev/schematics \
-H 'Content-Type: application/yaml' \
--data-binary 'customization:
systemExtensions:
officialExtensions:
- siderolabs/gvisor'
# Returns: {"id":"<id>","schematic":"..."}

# 2. Pull installer
docker pull factory.talos.dev/metal-installer/<id>:v1.7.0
```

List extensions available for a version:
```shell
curl https://factory.talos.dev/version/v1.7.0/extensions/official
```
20 changes: 20 additions & 0 deletions internal/frontend/http/llmstxt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package http

import (
"context"
"net/http"

"github.com/julienschmidt/httprouter"
)

// handleLLMsTxt serves the llms.txt file describing the Image Factory API for LLM agents.
func (f *Frontend) handleLLMsTxt(_ context.Context, w http.ResponseWriter, _ *http.Request, _ httprouter.Params) error {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
_, err := w.Write(getLLMsTxt())

return err
}
32 changes: 32 additions & 0 deletions internal/frontend/http/llmstxt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package http_test

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"

frontendhttp "github.com/siderolabs/image-factory/internal/frontend/http"
)

func TestHandleLLMsTxt(t *testing.T) {
frontend := frontendhttp.NewTestFrontend(zap.NewNop())

w := httptest.NewRecorder()
r := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/llms.txt", nil)

frontend.WrapHandler(frontend.HandleLLMsTxt())(w, r, nil)

resp := w.Result()
require.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, "text/plain; charset=utf-8", resp.Header.Get("Content-Type"))
assert.NotEmpty(t, w.Body.Bytes())
assert.Contains(t, w.Body.String(), "factory.talos.dev")
}
9 changes: 9 additions & 0 deletions internal/frontend/http/ui_debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ var (
localesFS = os.DirFS(basePath).(fs.ReadDirFS)
)

func getLLMsTxt() []byte {
content, err := os.ReadFile(basePath + "llms.txt")
if err != nil {
panic(err)
}

return content
}

func getTemplates() *template.Template {
// reload templates each time
return template.Must(template.New("").Funcs(templateFuncs).ParseFS(templatesFS, "templates/*.html"))
Expand Down
5 changes: 5 additions & 0 deletions internal/frontend/http/ui_embed.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ var templatesFS embed.FS
//go:embed locales/*.yaml
var localesFS embed.FS

//go:embed llms.txt
var llmsTxtContent []byte

func getLLMsTxt() []byte { return llmsTxtContent }

var templatesOnce = sync.OnceValue(func() *template.Template {
return template.Must(template.New("").Funcs(templateFuncs).ParseFS(templatesFS, "templates/*.html"))
})
Expand Down
Loading