Compare commits

..

4 Commits

Author SHA1 Message Date
Jeff McCune
f20f2fd203 show: compile build plans with sub-processes
This is a first stab at compiling build plans with a pool of holos
compile sub-processes.  Tested against the kargo-demo repository.
2025-05-05 19:43:39 -07:00
Jeff McCune
07f0ddd7b2 compile: take a BuildPlanRequest as input
This package clarifies and simplifies the input protocol of the holos
compile command.  A BuildPlanRequest represents the complete context
necessary to compile a BuildPlan.

This patch also upgrades cue to v0.13.0-alpha4
2025-05-05 19:43:37 -07:00
Jeff McCune
72d465fecc compile: export a build plan to stdout
This patch copies the build plan rendering from the show buildplans
command to implement the rendering in the compile subcommand.
2025-05-05 19:43:37 -07:00
Jeff McCune
1fc2f28289 compile: add basic structure of holos compile command
This command reads Component objects from a reader, exports a BuildPlan
from CUE, then marshals the result to the writer.

The purpose is to run concurrent instances of CUE to speed up build plan
generation prior to task execution.
2025-05-05 19:43:36 -07:00
25 changed files with 829 additions and 305 deletions

3
.gitignore vendored
View File

@@ -16,5 +16,4 @@ node_modules/
# nix
/.direnv/
result
.aider*
result

View File

@@ -1,110 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Holos is a configuration management tool for Kubernetes that implements the rendered manifests pattern using CUE. It unifies Helm charts, Kustomize bases, and raw Kubernetes manifests into a single, declarative pipeline.
### Core Flow
```
Platform → Components → BuildPlan → Generators → Transformers → Validators → Manifests
```
## Key Commands
```bash
# Development
make build # Build the binary
make test # Run all tests
make fmt # Format Go code
make lint # Run linters
make coverage # Generate coverage report
# Documentation
make update-docs # Update generated docs
make website # Build the documentation website
# Usage
holos render platform # Render entire platform
holos render component # Render single component
holos show buildplans # Show build plans
holos init platform # Initialize new platform
```
## Architecture
### Directory Structure
- `/api/` - API definitions (v1alpha5 stable, v1alpha6 in development)
- `/cmd/` - CLI entry point
- `/internal/cli/` - Command implementations
- `/internal/component/` - Component handling logic
- `/internal/platform/` - Platform handling logic
- `/internal/generate/` - Code generation
### Key Files
- `/internal/cli/render/render.go` - Core render logic
- `/internal/component/component.go` - Component processing
- `/api/core/v1alpha*/types.go` - API type definitions
### Component Types
1. **Helm** - Wraps Helm charts
2. **Kustomize** - Wraps Kustomize bases
3. **Kubernetes** - Raw Kubernetes manifests
## CUE Patterns
Components are defined in CUE:
```cue
package holos
holos: Component.BuildPlan
Component: #Helm & {
Name: "example"
Chart: {
version: "1.0.0"
repository: {
name: "example"
url: "https://charts.example.com"
}
}
}
```
## Testing
- Unit tests: `*_test.go` files colocated with source
- Integration tests: `/cmd/holos/tests/`
- Example platforms: `/internal/testutil/fixtures/`
- Run single test: `go test -run TestName ./path/to/package`
## Development Patterns
1. Error handling: Use `internal/errors/` types, wrap with context
2. Logging: Use structured `slog`, get logger with `logger.FromContext(ctx)`
3. CLI commands: Follow Cobra patterns in `/internal/cli/`
4. CUE formatting: Always run `cue fmt` on CUE files
5. Develop against v1alpha6 packages.
6. Commits: Use the package name as the first word in the commit, lower case. Commit without asking permission.
## Version Management
- Version files: `/version/embedded/{major,minor,patch}`
- Bump version: `make bump`
- API versions: v1alpha5 (stable), v1alpha6 (development)
## Key Concepts
- **Platform**: Top-level configuration containing all components
- **Component**: Unit of configuration (Helm/Kustomize/Kubernetes)
- **BuildPlan**: Instructions for building a component
- **Generator**: Creates manifests (Helm, Kustomize, etc.)
- **Transformer**: Modifies generated manifests
- **Validator**: Validates final manifests
## Resources
- Tutorials: `/doc/md/tutorial/`
- Platform templates: `/internal/generate/platforms/`
- Test fixtures: `/internal/testutil/fixtures/`

View File

@@ -27,9 +27,9 @@ import core "github.com/holos-run/holos/api/core/v1alpha6"
// platform command. Use the Components field to register components with the
// platform.
type Platform struct {
Name string
Components map[NameLabel]core.Component
Resource core.Platform
Name string `json:"name" yaml:"name" cue:"string | *\"default\""`
Components map[NameLabel]core.Component `json:"components" yaml:"components"`
Resource core.Platform `json:"resource" yaml:"resource"`
}
// ComponentConfig represents the configuration common to all kinds of

View File

@@ -46,10 +46,10 @@ const ComponentAnnotationsTag = "holos_component_annotations"
//
// [external credential provider]: https://github.com/kubernetes/enhancements/blob/313ad8b59c80819659e1fbf0f165230f633f2b22/keps/sig-auth/541-external-credential-providers/README.md
type BuildPlan struct {
// Kind represents the type of the resource.
Kind string `json:"kind" yaml:"kind" cue:"\"BuildPlan\""`
// APIVersion represents the versioned schema of the resource.
APIVersion string `json:"apiVersion" yaml:"apiVersion" cue:"\"v1alpha6\""`
// Kind represents the type of the resource.
Kind string `json:"kind" yaml:"kind" cue:"\"BuildPlan\""`
// Metadata represents data about the resource such as the Name.
Metadata Metadata `json:"metadata" yaml:"metadata"`
// Spec specifies the desired state of the resource.
@@ -412,10 +412,10 @@ type Metadata struct {
//
// cue export --out yaml ./platform
type Platform struct {
// Kind is a string value representing the resource.
Kind string `json:"kind" yaml:"kind" cue:"\"Platform\""`
// APIVersion represents the versioned schema of this resource.
APIVersion string `json:"apiVersion" yaml:"apiVersion" cue:"string | *\"v1alpha6\""`
// Kind is a string value representing the resource.
Kind string `json:"kind" yaml:"kind" cue:"\"Platform\""`
// Metadata represents data about the resource such as the Name.
Metadata Metadata `json:"metadata" yaml:"metadata"`

View File

@@ -180,9 +180,9 @@ Platform assembles a core Platform in the Resource field for the holos render pl
```go
type Platform struct {
Name string
Components map[NameLabel]core.Component
Resource core.Platform
Name string `json:"name" yaml:"name" cue:"string | *\"default\""`
Components map[NameLabel]core.Component `json:"components" yaml:"components"`
Resource core.Platform `json:"resource" yaml:"resource"`
}
```

View File

@@ -209,10 +209,10 @@ Holos uses CUE to construct a BuildPlan. Holos injects late binding values such
```go
type BuildPlan struct {
// Kind represents the type of the resource.
Kind string `json:"kind" yaml:"kind" cue:"\"BuildPlan\""`
// APIVersion represents the versioned schema of the resource.
APIVersion string `json:"apiVersion" yaml:"apiVersion" cue:"\"v1alpha6\""`
// Kind represents the type of the resource.
Kind string `json:"kind" yaml:"kind" cue:"\"BuildPlan\""`
// Metadata represents data about the resource such as the Name.
Metadata Metadata `json:"metadata" yaml:"metadata"`
// Spec specifies the desired state of the resource.
@@ -491,10 +491,10 @@ cue export --out yaml ./platform
```go
type Platform struct {
// Kind is a string value representing the resource.
Kind string `json:"kind" yaml:"kind" cue:"\"Platform\""`
// APIVersion represents the versioned schema of this resource.
APIVersion string `json:"apiVersion" yaml:"apiVersion" cue:"string | *\"v1alpha6\""`
// Kind is a string value representing the resource.
Kind string `json:"kind" yaml:"kind" cue:"\"Platform\""`
// Metadata represents data about the resource such as the Name.
Metadata Metadata `json:"metadata" yaml:"metadata"`

View File

@@ -1,52 +0,0 @@
# Compare Buildplans
Use the `holos compare buildplans <f1> <f2>` command to compare two BuildPlan
Files. Useful to ensure different configuration versions produce the same
results.
The `holos show buildplans` command writes a BuildPlan File to standard output.
A BuildPlan File is a yaml encoded stream of BuildPlan objects.
## User Requirements
1. `holos compare buildplans before.yaml after.yaml` must return exit code 1 when after.yaml contains fields (recursively) not present in before.yaml
2. `holos compare buildplans before.yaml after.yaml --backwards-compatible` must return exit code 0 when after.yaml contains fields (recursively) not present in before.yaml
## Behavior Specification
BuildPlan File f1 is equivalent to f2 when:
1. f1 and f2 have an equal number of BuildPlan objects.
2. each object in f1 is equivalent to exactly one unique object in f2.
Two BuildPlans, bp1 and bp2, are equivalent when:
1. All field values in bp1 are equivalent to the same field in bp2.
2. Both 1 and 2 apply to nested objects, recursively.
3. Field f is equivalent when bp1.f exactly equals bp2.f, except for:
3.1. Objects in the spec.artifacts list may appear in any arbitrary order.
3.2. The ordering of keys does not matter.
4. Backwards compatibility behavior (controlled by isBackwardsCompatible):
- When false: bp2 and bp1 must have exactly the same fields
- When true: bp2 may have additional fields that don't exist in bp1
(e.g., new features added in a newer version)
Example:
bp1 has {name: "x", version: "1.0"}
bp2 has {name: "x", version: "1.0", newFeature: "enabled"}
This comparison passes when isBackwardsCompatible=true
5. Fields in bp1 must always be present in bp2 (regardless of backwards
compatibility mode).
6. List type fields with a null value are equivalent to:
6.1. null values
6.2. empty values ([])
6.2. a missing field
A BuildPlan File is valid when:
1. Two or more identical objects exist in the same file. They must be
treated as unique objects when comparing BuildPlan Files
2. Two objects may have the same value for the metadata.name field.
3. The kind field of all objects in the file stream is "BuildPlan"
## Implementation Guidance
1. Implement a stub Comparer struct in the internal/compare package.
2. Implement a stub Comparer.BuildPlans() method.
3. Write test cases for each item in the Behavior Specification section. Use a table test approach that loads each test case from a subdirectory and reads the test case data from a `testcase.json` file. The json file should have an exitCode, name, msg, file1 and file2 fields. file1 is "before.yaml" and file2 is "after.yaml".
4. Modify the Comparer.BuildPlans() method to satisfy each test case.
5. Using the existing commands as an example, wire up the command line to the compare package.

42
go.mod
View File

@@ -1,28 +1,28 @@
module github.com/holos-run/holos
go 1.23
go 1.23.0
toolchain go1.23.2
toolchain go1.23.6
require (
cuelang.org/go v0.12.0
cuelang.org/go v0.13.0-alpha.4
github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-runewidth v0.0.15
github.com/olekukonko/tablewriter v0.0.5
github.com/princjef/gomarkdoc v1.1.0
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
github.com/rogpeppe/go-internal v1.14.1
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6
github.com/stretchr/testify v1.10.0
golang.org/x/sync v0.10.0
golang.org/x/tools v0.29.0
golang.org/x/sync v0.13.0
golang.org/x/tools v0.32.0
gopkg.in/yaml.v3 v3.0.1
helm.sh/helm/v3 v3.16.3
sigs.k8s.io/kustomize/kustomize/v5 v5.5.0
)
require (
cuelabs.dev/go/oci/ociregistry v0.0.0-20241125120445-2c00c104c6e1 // indirect
cuelabs.dev/go/oci/ociregistry v0.0.0-20250304105642-27e071d2c9b1 // indirect
dario.cat/mergo v1.0.1 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
@@ -57,7 +57,7 @@ require (
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/emicklei/proto v1.13.4 // indirect
github.com/emicklei/proto v1.14.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/evanphx/json-patch v5.9.0+incompatible // indirect
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
@@ -81,7 +81,7 @@ require (
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20240622144329-c177fd99eaa9 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
@@ -128,8 +128,8 @@ require (
github.com/nxadm/tail v1.4.11 // indirect
github.com/onsi/ginkgo v1.16.4 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
@@ -140,7 +140,7 @@ require (
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d // indirect
github.com/protocolbuffers/txtpbfmt v0.0.0-20250129171521-feedd8250727 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rubenv/sql-migrate v1.7.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
@@ -168,14 +168,14 @@ require (
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/oauth2 v0.25.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/term v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/oauth2 v0.29.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/term v0.31.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/grpc v1.65.0 // indirect

78
go.sum
View File

@@ -1,7 +1,7 @@
cuelabs.dev/go/oci/ociregistry v0.0.0-20241125120445-2c00c104c6e1 h1:mRwydyTyhtRX2wXS3mqYWzR2qlv6KsmoKXmlz5vInjg=
cuelabs.dev/go/oci/ociregistry v0.0.0-20241125120445-2c00c104c6e1/go.mod h1:5A4xfTzHTXfeVJBU6RAUf+QrlfTCW+017q/QiW+sMLg=
cuelang.org/go v0.12.0 h1:q4W5I+RtDIA27rslQyyt6sWkXX0YS9qm43+U1/3e0kU=
cuelang.org/go v0.12.0/go.mod h1:B4+kjvGGQnbkz+GuAv1dq/R308gTkp0sO28FdMrJ2Kw=
cuelabs.dev/go/oci/ociregistry v0.0.0-20250304105642-27e071d2c9b1 h1:Dmbd5Q+ENb2C6carvwrMsrOUwJ9X9qfL5JdW32gYAHo=
cuelabs.dev/go/oci/ociregistry v0.0.0-20250304105642-27e071d2c9b1/go.mod h1:dqrnoZx62xbOZr11giMPrWbhlaV8euHwciXZEy3baT8=
cuelang.org/go v0.13.0-alpha.4 h1:tmMJdmPCh6Y1vCPbvlsrG/g4Un3AnLajTScNDhLGMwU=
cuelang.org/go v0.13.0-alpha.4/go.mod h1:kA/8D9/d32IBp2lISWo90BEJhLyS9tqBcvqw2QAEhEg=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
@@ -82,7 +82,7 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8=
@@ -115,8 +115,8 @@ github.com/elazarl/goproxy v1.2.1 h1:njjgvO6cRG9rIqN2ebkqy6cQz2Njkx7Fsfv/zIZqgug
github.com/elazarl/goproxy v1.2.1/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emicklei/proto v1.13.4 h1:myn1fyf8t7tAqIzV91Tj9qXpvyXXGXk8OS2H6IBSc9g=
github.com/emicklei/proto v1.13.4/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
github.com/emicklei/proto v1.14.0 h1:WYxC0OrBuuC+FUCTZvb8+fzEHdZMwLEF+OnVfZA3LXU=
github.com/emicklei/proto v1.14.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls=
@@ -206,8 +206,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -362,10 +362,10 @@ github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI=
@@ -404,15 +404,15 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d h1:HWfigq7lB31IeJL8iy7jkUmU/PG1Sr8jVGhS749dbUA=
github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c=
github.com/protocolbuffers/txtpbfmt v0.0.0-20250129171521-feedd8250727 h1:A8EM8fVuYc0qbVMw9D6EiKdKTIm1SmLvAWcCc2mipGY=
github.com/protocolbuffers/txtpbfmt v0.0.0-20250129171521-feedd8250727/go.mod h1:VmWrOlMnBZNtToCWzRlZlIXcJqjo0hS5dwQbRD62gL8=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a h1:w3tdWGKbLGBPtR/8/oO74W6hmz0qE5q0z9aqSAewaaM=
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rubenv/sql-migrate v1.7.0 h1:HtQq1xyTN2ISmQDggnh0c9U3JlP8apWh8YO2jzlXpTI=
github.com/rubenv/sql-migrate v1.7.0/go.mod h1:S4wtDEG1CKn+0ShpTtzWhFpHHI5PvCUtiGI+C+Z2THE=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
@@ -437,10 +437,10 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -500,14 +500,14 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -517,18 +517,18 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -558,16 +558,16 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -575,8 +575,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -13,7 +13,7 @@ func New(name string) *cobra.Command {
cmd := &cobra.Command{
Use: name,
Short: name,
Version: version.Version,
Version: version.GetVersion(),
Args: cobra.NoArgs,
CompletionOptions: cobra.CompletionOptions{
HiddenDefaultCmd: true,

27
internal/cli/compile.go Normal file
View File

@@ -0,0 +1,27 @@
package cli
import (
_ "embed"
"github.com/holos-run/holos/internal/cli/command"
"github.com/holos-run/holos/internal/compile"
"github.com/holos-run/holos/internal/errors"
"github.com/spf13/cobra"
)
//go:embed compile.txt
var compileLong string
// NewCompileCmd returns a new compile command.
func NewCompileCmd() *cobra.Command {
cmd := command.New("compile")
cmd.Short = "Compile Components (stdin) to BuildPlans (stdout) using CUE"
cmd.Long = compileLong
cmd.Args = cobra.NoArgs
cmd.RunE = func(cmd *cobra.Command, args []string) error {
c := compile.New()
ctx := cmd.Root().Context()
return errors.Wrap(c.Run(ctx))
}
return cmd
}

17
internal/cli/compile.txt Normal file
View File

@@ -0,0 +1,17 @@
Reads a stream of JSON-encoded Component objects from standard input. For each
Component, builds a CUE instance then exports a BuildPlan. Each resulting
BuildPlan is written as a JSON-encoded object to standard output.
This command encapsulates cue export for concurrent use. It may be used directly
by piping the output of holos show platform through jq to select the components
list, then to stdin of this command. This command is most often invoked 'holos
render platform' to run concurrent cue exports safely.
For example:
holos show platform --format=json \
| jq '.spec.components[] | {kind: "Component", apiVersion: "v1alpha6", component: .}' \
| holos compile --log-level=debug
Note each platform components element is embedded into the component field of an
enveloping object for the purpose of conveying type metadata.

View File

@@ -76,6 +76,9 @@ func New(cfg *holos.Config) *cobra.Command {
// Show
rootCmd.AddCommand(NewShowCmd(platform.NewConfig()))
// Compile
rootCmd.AddCommand(NewCompileCmd())
return rootCmd
}

View File

@@ -3,10 +3,15 @@ package cli
import (
"context"
_ "embed"
"encoding/json"
"fmt"
"io"
"log/slog"
v1alpha5 "github.com/holos-run/holos/api/core/v1alpha5"
v1alpha6 "github.com/holos-run/holos/api/core/v1alpha6"
"github.com/holos-run/holos/internal/cli/command"
"github.com/holos-run/holos/internal/component"
"github.com/holos-run/holos/internal/compile"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/holos"
"github.com/holos-run/holos/internal/platform"
@@ -77,37 +82,62 @@ func (s *showBuildPlans) flagSet() *pflag.FlagSet {
}
func (s *showBuildPlans) Run(ctx context.Context, p *platform.Platform) error {
encoder, err := holos.NewSequentialEncoder(s.format, s.cfg.Stdout)
components := p.Select(s.cfg.ComponentSelectors...)
reqs := make([]compile.BuildPlanRequest, len(components))
for idx, c := range components {
tags, err := c.Tags()
if err != nil {
return errors.Wrap(err)
}
reqs[idx] = compile.BuildPlanRequest{
APIVersion: "v1alpha6",
Kind: "BuildPlanRequest",
Root: p.Root(),
Leaf: c.Path(),
WriteTo: s.cfg.WriteTo,
TempDir: "${TMPDIR_PLACEHOLDER}",
Tags: tags,
}
}
resp, err := compile.Compile(ctx, s.cfg.Concurrency, reqs)
if err != nil {
return errors.Wrap(err)
}
defer encoder.Close()
opts := platform.BuildOpts{
PerComponentFunc: func(ctx context.Context, idx int, pc holos.Component) error {
c := component.New(p.Root(), pc.Path(), component.NewConfig())
tm, err := c.TypeMeta()
if err != nil {
return errors.Wrap(err)
}
opts := holos.NewBuildOpts(p.Root(), pc.Path(), s.cfg.WriteTo, "${TMPDIR_PLACEHOLDER}")
// TODO(jjm): refactor into [holos.NewBuildOpts] as functional options.
// Component name, label, annotations passed via tags to cue.
tags, err := pc.Tags()
if err != nil {
return errors.Wrap(err)
}
opts.Tags = tags
bp, err := c.BuildPlan(tm, opts)
if err != nil {
return errors.Wrap(err)
}
// Export the build plan using the sequential encoder.
return errors.Wrap(bp.Export(idx, encoder))
},
encoder, err := holos.NewEncoder(s.format, s.cfg.Stdout)
if err != nil {
return errors.Wrap(err)
}
return errors.Wrap(p.Build(ctx, opts))
for _, buildPlanResponse := range resp {
var tm holos.TypeMeta
if err := json.Unmarshal(buildPlanResponse.RawMessage, &tm); err != nil {
return errors.Format("could not discriminate type meta: %w", err)
}
if tm.Kind != "BuildPlan" {
return errors.Format("invalid kind %s: must be BuildPlan", tm.Kind)
}
var buildPlan any
switch tm.APIVersion {
case "v1alpha5":
buildPlan = &v1alpha5.BuildPlan{}
case "v1alpha6":
buildPlan = &v1alpha6.BuildPlan{}
default:
slog.WarnContext(ctx, fmt.Sprintf("unknown BuildPlan APIVersion %s: assuming v1alpha6 schema", tm.APIVersion))
buildPlan = &v1alpha6.BuildPlan{}
}
if err := json.Unmarshal(buildPlanResponse.RawMessage, buildPlan); err != nil {
return errors.Wrap(err)
}
if err := encoder.Encode(buildPlan); err != nil {
return errors.Wrap(err)
}
}
return nil
}

250
internal/compile/compile.go Normal file
View File

@@ -0,0 +1,250 @@
// Package compile compiles BuildPlan resources by reading json encoded data
// from a reader, unmarshaling the data into a Component, building a CUE
// instance injecting the Component as a tag, then exporting a BuildPlan and
// marshalling the result to a writer represented as a stream of json objects.
// Each input component maps to one output json object in the stream.
package compile
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"os"
"os/exec"
"time"
componentPkg "github.com/holos-run/holos/internal/component"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/holos"
"github.com/holos-run/holos/internal/logger"
"golang.org/x/sync/errgroup"
)
// BuildPlanRequest represents the complete context necessary to produce a
// BuildPlan. BuildPlanRequest is the primary input to the holos compile
// command, read from standard input. Provided by the holos render platform
// command.
type BuildPlanRequest struct {
APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"`
Kind string `json:"kind,omitempty" yaml:"kind,omitempty"`
Root string `json:"root,omitempty" yaml:"root,omitempty"`
Leaf string `json:"leaf,omitempty" yaml:"leaf,omitempty"`
WriteTo string `json:"writeTo,omitempty" yaml:"writeTo,omitempty"`
TempDir string `json:"tempDir,omitempty" yaml:"tempDir,omitempty"`
Tags []string
}
type BuildPlanResponse struct {
APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"`
Kind string `json:"kind,omitempty" yaml:"kind,omitempty"`
RawMessage json.RawMessage `json:"rawMessage,omitempty" yaml:"rawMessage,omitempty"`
}
// New returns a new BuildPlan Compiler.
func New() *Compiler {
return &Compiler{
R: os.Stdin,
W: os.Stdout,
Encoding: "json",
}
}
// Compiler reads BuildPlanRequest objects from R and exports
// BuildPlanResponse objects to W.
type Compiler struct {
R io.Reader
W io.Writer
// Encoding specifies "json" (default) or "yaml" format output.
Encoding string
}
// Run reads a BuildPlanRequest from R and compiles a BuildPlan into a
// BuildPlanResponse on W. R and W are usually connected to stdin and stdout.
func (c *Compiler) Run(ctx context.Context) error {
epoch := time.Now()
decoder := json.NewDecoder(c.R)
encoder, err := holos.NewSequentialEncoder(c.Encoding, c.W)
if err != nil {
return errors.Wrap(err)
}
slog.DebugContext(ctx, "entering read loop")
for idx := 0; ; idx++ {
log := slog.With("idx", idx)
select {
case <-ctx.Done():
return errors.Wrap(ctx.Err())
default:
}
var raw json.RawMessage
err := decoder.Decode(&raw)
if err == io.EOF {
duration := time.Since(epoch)
msg := fmt.Sprintf("received eof: exiting: total runtime %.3fs", duration.Seconds())
log.DebugContext(ctx, msg, "eof", true, "seconds", duration.Seconds())
return nil
}
start := time.Now()
var meta holos.TypeMeta
err = json.Unmarshal(raw, &meta)
if err != nil {
return errors.Format("could not unmarshal type meta: %w", err)
}
log.DebugContext(ctx, fmt.Sprintf("received: %+v", meta), "meta", meta)
var req BuildPlanRequest
// platform component from the components field of the platform resource.
switch meta.APIVersion {
case "v1alpha6":
switch meta.Kind {
case holos.BuildPlanRequest:
err = json.Unmarshal(raw, &req)
if err != nil {
return errors.Format("could not unmarshal %+v: %w", meta, err)
}
default:
return errors.Format("unsupported kind: %+v (ignored)", meta)
}
default:
return errors.Format("unsupported api version: %+v (ignored)", meta)
}
// Produce the build plan.
component := componentPkg.New(req.Root, req.Leaf, componentPkg.NewConfig())
tm, err := component.TypeMeta()
if err != nil {
return errors.Wrap(err)
}
// TODO(jjm): Decide how to handle the temp directory.
opts := holos.NewBuildOpts(req.Root, req.Leaf, req.WriteTo, "${TMPDIR_PLACEHOLDER}")
// Component name, label, annotations passed via tags to cue.
opts.Tags = req.Tags
bp, err := component.BuildPlan(tm, opts)
if err != nil {
return errors.Wrap(err)
}
if err := bp.Export(idx, encoder); err != nil {
return errors.Format("could not marshal output: %w", err)
}
duration := time.Since(start)
log.DebugContext(ctx, fmt.Sprintf("compile time: %.3fs", duration.Seconds()))
}
}
type task struct {
idx int
req BuildPlanRequest
resp BuildPlanResponse
}
func Compile(ctx context.Context, concurrency int, reqs []BuildPlanRequest) (resp []BuildPlanResponse, err error) {
concurrency = min(len(reqs), max(1, concurrency))
resp = make([]BuildPlanResponse, len(reqs))
g, ctx := errgroup.WithContext(ctx)
tasks := make(chan task)
// Producer
g.Go(func() error {
for idx, req := range reqs {
tsk := task{
idx: idx,
req: req,
resp: BuildPlanResponse{},
}
select {
case <-ctx.Done():
return errors.Wrap(ctx.Err())
case tasks <- tsk:
slog.DebugContext(ctx, fmt.Sprintf("producer producing task seq=%d component=%s tags=%+v", tsk.idx, tsk.req.Leaf, tsk.req.Tags))
}
}
slog.DebugContext(ctx, fmt.Sprintf("producer finished: closing tasks channel"))
close(tasks)
return nil
})
// Consumers
for id := range concurrency {
g.Go(func() error {
return compiler(ctx, id, tasks, resp)
})
}
err = errors.Wrap(g.Wait())
return
}
func compiler(ctx context.Context, id int, tasks chan task, resp []BuildPlanResponse) error {
log := logger.FromContext(ctx).With("id", id)
// Start the sub-process
exe, err := os.Executable()
if err != nil {
return errors.Wrap(err)
}
cmd := exec.CommandContext(ctx, exe, "compile")
stdinPipe, err := cmd.StdinPipe()
if err != nil {
return errors.Format("could not attach to stdin for worker %d: %w", id, err)
}
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
_ = stdinPipe.Close()
return errors.Format("could not attach to stdout for worker %d: %w", id, err)
}
var stderrBuf bytes.Buffer
cmd.Stderr = &stderrBuf
if err := cmd.Start(); err != nil {
_ = stdinPipe.Close()
return errors.Format("could not start worker %d: %w", id, err)
}
pid := cmd.Process.Pid
msg := fmt.Sprintf("compiler id=%d pid=%d", id, pid)
log.DebugContext(ctx, fmt.Sprintf("%s: started", msg))
defer func() {
stdinPipe.Close()
if err := cmd.Wait(); err != nil {
log.ErrorContext(ctx, fmt.Sprintf("%s: exited uncleanly: %s", msg, err), "err", err, "stderr", stderrBuf.String())
}
}()
encoder := json.NewEncoder(stdinPipe)
decoder := json.NewDecoder(stdoutPipe)
for {
select {
case <-ctx.Done():
return errors.Wrap(ctx.Err())
case tsk, ok := <-tasks:
if !ok {
log.DebugContext(ctx, fmt.Sprintf("%s: tasks channel closed: returning normally", msg))
return nil
}
log.DebugContext(ctx, fmt.Sprintf("%s: encoding request seq=%d", msg, tsk.idx))
if err := encoder.Encode(tsk.req); err != nil {
return errors.Format("could not encode request for %s: %w", msg, err)
}
log.DebugContext(ctx, fmt.Sprintf("%s: decoding response seq=%d", msg, tsk.idx))
if err := decoder.Decode(&resp[tsk.idx].RawMessage); err != nil {
return errors.Format("could not decode response from %s: %w\n%s", msg, err, stderrBuf.String())
}
log.DebugContext(ctx, fmt.Sprintf("%s: ok finished task seq=%d", msg, tsk.idx))
}
}
}

View File

@@ -0,0 +1,358 @@
//go:build gemini
package compile
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"runtime" // To get number of CPUs
"golang.org/x/sync/errgroup"
)
// Placeholder types - replace with your actual definitions
type Component struct {
ID int `json:"id"` // Add an ID to correlate requests/responses if needed later
Name string `json:"name"`
Data string `json:"data"`
// Add other fields as needed
}
type BuildPlan struct {
ComponentID int `json:"componentId"` // Correlates back to Component.ID
ComponentUsed string `json:"componentUsed"`
Result map[string]interface{} `json:"result"`
Success bool `json:"success"`
WorkerPID int `json:"workerPid"` // Optional: useful for debugging
// Add other fields as needed
}
// executeReusableWorkers starts a pool of worker processes, distributes components
// to them, and collects build plans. Uses line-delimited JSON over stdio.
func executeReusableWorkers(ctx context.Context, numWorkers int, components []Component) ([]BuildPlan, error) {
if len(components) == 0 {
return nil, nil
}
if numWorkers <= 0 {
numWorkers = runtime.NumCPU() // Default to number of CPUs
}
if numWorkers > len(components) {
numWorkers = len(components) // No need for more workers than tasks
}
exePath, err := os.Executable()
if err != nil {
return nil, fmt.Errorf("failed to get executable path: %w", err)
}
// Channel for distributing components to worker goroutines
// Buffer size == len(components) to avoid blocking the sender initially
tasksChan := make(chan Component, len(components))
// Channel for collecting results from worker goroutines
// Buffer size helps prevent worker goroutines blocking if main thread is slow
resultsChan := make(chan BuildPlan, len(components))
// Use an error group to manage worker goroutines and capture the first error
g, childCtx := errgroup.WithContext(ctx)
// Start worker processes and their managing goroutines
for i := 0; i < numWorkers; i++ {
workerIndex := i // Capture loop variable
// Create the command for the worker process
// Ensure the child knows it's a worker (e.g., via "--worker" flag)
cmd := exec.CommandContext(childCtx, exePath, "--worker") // Adapt flag as needed
stdinPipe, err := cmd.StdinPipe()
if err != nil {
return nil, fmt.Errorf("worker %d: failed to get stdin pipe: %w", workerIndex, err)
}
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
_ = stdinPipe.Close()
return nil, fmt.Errorf("worker %d: failed to get stdout pipe: %w", workerIndex, err)
}
var stderrBuf bytes.Buffer
cmd.Stderr = &stderrBuf
// Start the worker process
if err := cmd.Start(); err != nil {
_ = stdinPipe.Close()
// stdoutPipe closing is less critical before Wait
return nil, fmt.Errorf("worker %d: failed to start command '%s': %w", workerIndex, exePath, err)
}
pid := cmd.Process.Pid // Get PID for logging/debugging
fmt.Printf("Parent: Started worker %d (PID: %d)\n", workerIndex, pid)
// Launch a goroutine to manage this specific worker
g.Go(func() error {
// Ensure resources are cleaned up for this worker's goroutine
defer func() {
fmt.Printf("Parent: Goroutine for worker %d (PID: %d) shutting down stdin.\n", workerIndex, pid)
// Closing stdin signals the worker to terminate its loop
stdinPipe.Close()
// Wait for the process to fully exit after stdin is closed
waitErr := cmd.Wait()
stderrContent := stderrBuf.String()
if waitErr != nil {
fmt.Fprintf(os.Stderr, "Parent: Worker %d (PID: %d) exited with error (stderr: %q): %v\n", workerIndex, pid, stderrContent, waitErr)
// Note: Returning an error here might race with errors from I/O below, errgroup handles this.
} else {
fmt.Printf("Parent: Worker %d (PID: %d) exited cleanly.\n", workerIndex, pid)
}
}()
// Use buffered I/O for efficiency with line-based protocol
writer := bufio.NewWriter(stdinPipe)
// Use bufio.Scanner for reading line-delimited output
scanner := bufio.NewScanner(stdoutPipe)
// Process tasks from the channel until it's closed and empty
for task := range tasksChan {
// Marshal component to JSON
jsonData, err := json.Marshal(task)
if err != nil {
return fmt.Errorf("worker %d (PID: %d): failed to marshal component %d: %w", workerIndex, pid, task.ID, err)
}
// Write JSON line to worker's stdin
// fmt.Printf("Parent: Sending task %d to worker %d (PID: %d)\n", task.ID, workerIndex, pid) // Debug logging
if _, err := writer.Write(jsonData); err != nil {
return fmt.Errorf("worker %d (PID: %d): failed to write component %d to stdin: %w", workerIndex, pid, task.ID, err)
}
if err := writer.WriteByte('\n'); err != nil { // Write newline delimiter
return fmt.Errorf("worker %d (PID: %d): failed to write newline to stdin for component %d: %w", workerIndex, pid, task.ID, err)
}
if err := writer.Flush(); err != nil { // Ensure data is sent
return fmt.Errorf("worker %d (PID: %d): failed to flush stdin for component %d: %w", workerIndex, pid, task.ID, err)
}
// Read line (JSON BuildPlan) from worker's stdout
if !scanner.Scan() {
// Scanner failed, check for errors or premature EOF
if err := scanner.Err(); err != nil {
return fmt.Errorf("worker %d (PID: %d): error scanning stdout after sending component %d: %w", workerIndex, pid, task.ID, err)
}
// If no scanner error, it means EOF was reached unexpectedly
return fmt.Errorf("worker %d (PID: %d): unexpected EOF reading stdout after sending component %d", workerIndex, pid, task.ID)
}
line := scanner.Bytes() // Get the line bytes
// Unmarshal the BuildPlan
var plan BuildPlan
if err := json.Unmarshal(line, &plan); err != nil {
return fmt.Errorf("worker %d (PID: %d): failed to unmarshal build plan (line: %q): %w", workerIndex, pid, string(line), err)
}
plan.WorkerPID = pid // Add worker PID for tracking
// Send the result back to the main goroutine
select {
case resultsChan <- plan:
// fmt.Printf("Parent: Received result for task %d from worker %d (PID: %d)\n", plan.ComponentID, workerIndex, pid) // Debug logging
case <-childCtx.Done():
return fmt.Errorf("worker %d (PID: %d): context cancelled while sending result for component %d: %w", workerIndex, pid, task.ID, childCtx.Err())
}
}
// tasksChan was closed and this worker processed all its assigned tasks
fmt.Printf("Parent: Worker %d (PID: %d) finished processing tasks.\n", workerIndex, pid)
return nil // Goroutine finished successfully
})
}
// Goroutine to distribute tasks
// This runs concurrently with the worker goroutines
go func() {
fmt.Printf("Parent: Distributing %d tasks...\n", len(components))
for i, comp := range components {
comp.ID = i // Assign a unique ID for potential correlation
select {
case tasksChan <- comp:
// Task sent
case <-childCtx.Done():
// Context cancelled before all tasks could be sent
fmt.Fprintf(os.Stderr, "Parent: Task distribution cancelled: %v\n", childCtx.Err())
// Closing tasksChan here ensures workers eventually stop asking for tasks
close(tasksChan)
return
}
}
// After sending all tasks, close the channel to signal workers
close(tasksChan)
fmt.Println("Parent: Finished distributing tasks and closed tasks channel.")
}()
// Wait for all worker goroutines to complete (or one to error out)
fmt.Println("Parent: Waiting for workers to finish...")
err = g.Wait() // Returns the first error encountered by any worker goroutine
// Close the results channel *after* all worker goroutines have finished
// This signals the final result collection step
fmt.Println("Parent: All worker goroutines finished or errored. Closing results channel.")
close(resultsChan)
// Collect all results sent by the workers
// This loop reads until resultsChan is closed
finalResults := make([]BuildPlan, 0, len(components))
for result := range resultsChan {
finalResults = append(finalResults, result)
}
fmt.Printf("Parent: Collected %d results.\n", len(finalResults))
// Return results even if there was an error, they might be partial
if err != nil {
// Log the primary error that caused the errgroup to exit
fmt.Fprintf(os.Stderr, "Parent: executeReusableWorkers returning with error: %v\n", err)
// The finalResults slice might contain results from before the error occurred
return finalResults, err
}
// Check if we got the expected number of results (only if no error occurred)
if len(finalResults) != len(components) {
return finalResults, fmt.Errorf("mismatch: expected %d results, got %d", len(components), len(finalResults))
}
return finalResults, nil // Success
}
// --- Child Worker Logic (Must be added to main.go) ---
// // Example main function incorporating the worker logic
// func main() {
// // Check if running as a worker
// if len(os.Args) > 1 && os.Args[1] == "--worker" {
// // Set GOMAXPROCS? Maybe not necessary if CUE isn't concurrent anyway.
// runWorker() // Run the dedicated worker function
// return // Important: worker exits via runWorker
// }
//
// // --- Parent Process Logic ---
// fmt.Println("Running as parent...")
// componentsToProcess := []Component{
// {Name: "CompA", Data: "data1"},
// {Name: "CompB", Data: "data2"},
// {Name: "CompC", Data: "data3"},
// {Name: "CompD", Data: "data4"},
// {Name: "CompE", Data: "data5"},
// }
//
// ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) // Example timeout
// defer cancel()
//
// // Use, for example, 2 worker processes
// buildPlans, err := executeReusableWorkers(ctx, 2, componentsToProcess)
// if err != nil {
// // Note: buildPlans might contain partial results even if err is non-nil
// fmt.Fprintf(os.Stderr, "Error executing reusable workers: %v\n", err)
// // Decide if partial results are useful or should be discarded
// // os.Exit(1) // Optionally exit
// }
//
// fmt.Printf("Parent: Received %d build plans:\n", len(buildPlans))
// // Note: Order is not guaranteed relative to input order.
// // Sort or process based on ComponentID if needed.
// sort.Slice(buildPlans, func(i, j int) bool {
// return buildPlans[i].ComponentID < buildPlans[j].ComponentID
// })
// for _, plan := range buildPlans {
// fmt.Printf(" Plan for Component %d (from worker %d): Success=%t, Result=%v\n",
// plan.ComponentID, plan.WorkerPID, plan.Success, plan.Result)
// }
// }
// // runWorker implements the logic for a child worker process.
// // It reads line-delimited JSON Components from stdin and writes
// // line-delimited JSON BuildPlans to stdout.
// func runWorker() {
// workerPID := os.Getpid()
// fmt.Fprintf(os.Stderr, "Worker (PID: %d): Starting\n", workerPID)
// // Use buffered I/O for stdin and stdout
// stdinScanner := bufio.NewScanner(os.Stdin)
// stdoutWriter := bufio.NewWriter(os.Stdout)
// defer stdoutWriter.Flush() // Ensure buffer is flushed on exit
// // Loop reading tasks line by line from stdin
// for stdinScanner.Scan() {
// line := stdinScanner.Bytes()
// if len(line) == 0 { // Skip empty lines if any occur
// continue
// }
// var comp Component
// if err := json.Unmarshal(line, &comp); err != nil {
// fmt.Fprintf(os.Stderr, "Worker (PID: %d): Failed to decode component (line: %q): %v\n", workerPID, string(line), err)
// // Decide strategy: exit? skip? For now, exit.
// os.Exit(1)
// }
// fmt.Fprintf(os.Stderr, "Worker (PID: %d): Processing component %d (%s)\n", workerPID, comp.ID, comp.Name)
// // --- Simulate CUE processing ---
// time.Sleep(time.Duration(500+rand.Intn(500)) * time.Millisecond) // Simulate variable work
// success := true
// resultData := map[string]interface{}{
// "processedData": fmt.Sprintf("Processed %s by %d", comp.Data, workerPID),
// "timestamp": time.Now().UnixNano(),
// }
// // --- End Simulation ---
// plan := BuildPlan{
// ComponentID: comp.ID, // Echo back the ID
// ComponentUsed: comp.Name,
// Result: resultData,
// Success: success,
// // WorkerPID added by parent, not needed here
// }
// // Marshal result to JSON
// planJSON, err := json.Marshal(plan)
// if err != nil {
// fmt.Fprintf(os.Stderr, "Worker (PID: %d): Failed to marshal build plan for component %d: %v\n", workerPID, comp.ID, err)
// // Decide strategy: exit? skip? For now, exit.
// os.Exit(1)
// }
// // Write JSON result line to stdout
// if _, err := stdoutWriter.Write(planJSON); err != nil {
// fmt.Fprintf(os.Stderr, "Worker (PID: %d): Failed to write build plan for component %d to stdout: %v\n", workerPID, comp.ID, err)
// os.Exit(1)
// }
// if err := stdoutWriter.WriteByte('\n'); err != nil { // Add newline delimiter
// fmt.Fprintf(os.Stderr, "Worker (PID: %d): Failed to write newline for component %d to stdout: %v\n", workerPID, comp.ID, err)
// os.Exit(1)
// }
// // Flush the buffer after each line to ensure parent receives it
// if err := stdoutWriter.Flush(); err != nil {
// fmt.Fprintf(os.Stderr, "Worker (PID: %d): Failed to flush stdout for component %d: %v\n", workerPID, comp.ID, err)
// os.Exit(1)
// }
// fmt.Fprintf(os.Stderr, "Worker (PID: %d): Finished processing component %d (%s)\n", workerPID, comp.ID, comp.Name)
// }
// // Check for scanner errors (e.g., read errors) after the loop finishes
// if err := stdinScanner.Err(); err != nil {
// // Don't report EOF as an error, it's the signal to exit cleanly
// if err != io.EOF {
// fmt.Fprintf(os.Stderr, "Worker (PID: %d): Error reading stdin: %v\n", workerPID, err)
// os.Exit(1)
// }
// }
// // EOF reached on stdin, parent closed the pipe. Exit cleanly.
// fmt.Fprintf(os.Stderr, "Worker (PID: %d): Stdin closed, exiting cleanly.\n", workerPID)
// os.Exit(0)
// }
// NOTE: Need these imports for the example usage and worker logic:
// import (
// "io"
// "math/rand"
// "sort"
// "time"
// )

View File

@@ -37,12 +37,8 @@ func NewConfig() Config {
cfg := Config{
Concurrency: runtime.NumCPU(),
TagMap: make(holos.TagMap),
WriteTo: os.Getenv(holos.WriteToEnvVar),
Stderr: os.Stderr,
}
if cfg.WriteTo == "" {
cfg.WriteTo = holos.WriteToDefault
}
return cfg
}

View File

@@ -68,7 +68,6 @@ func (i *Instance) HolosValue() (v cue.Value, err error) {
// Return the deprecated value at the root
return i.value, nil
}
err = errors.Wrap(err)
}
return
}

View File

@@ -29,7 +29,8 @@ import (
// the tests.
type Component struct {
Component core.Component
holos.TypeMeta
Component core.Component `json:"component,omitempty" yaml:"component,omitempty"`
}
func (c *Component) Describe() string {
@@ -802,20 +803,17 @@ func NewBuildContext(opts holos.BuildOpts) (*BuildContext, error) {
if !filepath.IsAbs(root) {
return nil, errors.Format("not an absolute path: %s", root)
}
exe, err := util.Executable()
holosExecutable, err := util.Executable()
if err != nil {
return nil, errors.Format("could not get holos path: %w", err)
return nil, errors.Format("could not get holos executable path: %w", err)
}
// Allow placeholder for idempotent holos show buildplan output.
tempDir := opts.TempDir()
bc := &BuildContext{
BuildContext: core.BuildContext{
TempDir: tempDir,
TempDir: opts.TempDir(),
RootDir: root,
LeafDir: opts.Leaf(),
HolosExecutable: exe,
HolosExecutable: holosExecutable,
},
}
return bc, nil
}

View File

@@ -29,9 +29,9 @@ import "github.com/holos-run/holos/api/core/v1alpha6:core"
// platform command. Use the Components field to register components with the
// platform.
#Platform: {
Name: string
Components: {[string]: core.#Component} @go(,map[NameLabel]core.Component)
Resource: core.#Platform
name: string & (string | *"default") @go(Name)
components: {[string]: core.#Component} @go(Components,map[NameLabel]core.Component)
resource: core.#Platform @go(Resource)
}
// ComponentConfig represents the configuration common to all kinds of

View File

@@ -34,12 +34,12 @@ package core
//
// [external credential provider]: https://github.com/kubernetes/enhancements/blob/313ad8b59c80819659e1fbf0f165230f633f2b22/keps/sig-auth/541-external-credential-providers/README.md
#BuildPlan: {
// Kind represents the type of the resource.
kind: string & "BuildPlan" @go(Kind)
// APIVersion represents the versioned schema of the resource.
apiVersion: string & "v1alpha6" @go(APIVersion)
// Kind represents the type of the resource.
kind: string & "BuildPlan" @go(Kind)
// Metadata represents data about the resource such as the Name.
metadata: #Metadata @go(Metadata)
@@ -432,12 +432,12 @@ package core
//
// cue export --out yaml ./platform
#Platform: {
// Kind is a string value representing the resource.
kind: string & "Platform" @go(Kind)
// APIVersion represents the versioned schema of this resource.
apiVersion: string & (string | *"v1alpha6") @go(APIVersion)
// Kind is a string value representing the resource.
kind: string & "Platform" @go(Kind)
// Metadata represents data about the resource such as the Name.
metadata: #Metadata @go(Metadata)

View File

@@ -6,11 +6,11 @@ import (
)
#Platform: {
Name: string | *"no-platform-name"
Components: _
Resource: core.#Platform & {
metadata: name: Name
spec: components: [for x in Components {x}]
name: _
components: _
resource: {
metadata: "name": name
spec: "components": [for x in components {x}]
}
}

View File

@@ -2,9 +2,9 @@ package holos
import "github.com/holos-run/holos/api/author/v1alpha6:author"
Platform: author.#Platform & {
Name: "default"
}
// holos represents the field holos render platform evaluates, the resource
// field of the author.#Platform definition constructed from a components
// struct.
holos: platform.resource
// Render a Platform resource for holos to process
holos: Platform.Resource
platform: author.#Platform

View File

@@ -12,3 +12,9 @@ const WriteToEnvVar string = "HOLOS_WRITE_TO"
// TypeMetaFile represents the file holos uses to discriminate the api version
// of a component BuildPlan.
const TypeMetaFile string = "typemeta.yaml"
// BuildPlanRequest represents the kind value of a BuildPlanRequest
const BuildPlanRequest string = "BuildPlanRequest"
// BuildPlanResponse represents the kind value of a BuildPlanResponse
const BuildPlanResponse string = "BuildPlanResponse"

View File

@@ -184,12 +184,14 @@ func (s *Selector) Set(value string) error {
// TypeMeta represents the kind and version of a resource holos needs to
// process. Useful to discriminate generated resources.
type TypeMeta struct {
Kind string `json:"kind,omitempty" yaml:"kind,omitempty"`
APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"`
Kind string `json:"kind,omitempty" yaml:"kind,omitempty"`
}
func NewSequentialEncoder(format string, w io.Writer) (OrderedEncoder, error) {
enc, err := NewEncoder(format, w)
// NewSequentialEncoder returns a yaml or json encoder that writes to w. The
// encoding argument may be "json" or "yaml".
func NewSequentialEncoder(encoding string, w io.Writer) (OrderedEncoder, error) {
enc, err := NewEncoder(encoding, w)
if err != nil {
return nil, err
}
@@ -200,7 +202,8 @@ func NewSequentialEncoder(format string, w io.Writer) (OrderedEncoder, error) {
return seqEnc, nil
}
// NewEncoder returns a yaml or json encoder that writes to w.
// NewEncoder returns a yaml or json encoder that writes to w. The format
// argument specifies "yaml" or "json" format output.
func NewEncoder(format string, w io.Writer) (Encoder, error) {
switch format {
case "yaml":
@@ -338,7 +341,7 @@ type BuildOpts struct {
// NewBuildOpts returns a [BuildOpts] configured to build the component at leaf
// from the platform module at root writing rendered manifests into the deploy
// directory.
func NewBuildOpts(root, leaf, deploy, tempDir string) BuildOpts {
func NewBuildOpts(root, leaf, writeTo, tempDir string) BuildOpts {
return BuildOpts{
Store: artifact.NewStore(),
Concurrency: min(runtime.NumCPU(), 8),
@@ -347,7 +350,7 @@ func NewBuildOpts(root, leaf, deploy, tempDir string) BuildOpts {
root: filepath.Clean(root),
leaf: filepath.Clean(leaf),
writeTo: filepath.Clean(deploy),
writeTo: filepath.Clean(writeTo),
tempDir: filepath.Clean(tempDir),
}
}