Skip to content

Commit b9a78e8

Browse files
feat: Add basic spec parsing (#78)
Co-authored-by: Mark Chmarny <mchmarny@nvidia.com> Co-authored-by: Mark Chmarny <mchmarny@users.noreply.github.com>
1 parent f43498b commit b9a78e8

File tree

14 files changed

+579
-12
lines changed

14 files changed

+579
-12
lines changed

.claude/CLAUDE.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,12 @@ make tools-check # Verify versions match .versions.yaml
6363

6464
1. **Read before writing** — Never modify code you haven't read
6565
2. **Tests must pass**`make test` with race detector; never skip tests
66-
3. **Use project patterns** — Learn existing code before inventing new approaches
67-
4. **3-strike rule** — After 3 failed fix attempts, stop and reassess
68-
5. **Structured errors** — Use `pkg/errors` with error codes (never `fmt.Errorf`)
69-
6. **Context timeouts** — All I/O operations need context with timeout
70-
7. **Check context in loops** — Always check `ctx.Done()` in long-running operations
66+
3. **Run `make qualify` often** — Run at every stopping point (after completing a phase, before commits, before moving on). Fix ALL lint/test failures before proceeding. Do not treat pre-existing failures as acceptable.
67+
4. **Use project patterns** — Learn existing code before inventing new approaches
68+
5. **3-strike rule** — After 3 failed fix attempts, stop and reassess
69+
6. **Structured errors** — Use `pkg/errors` with error codes (never `fmt.Errorf`)
70+
7. **Context timeouts** — All I/O operations need context with timeout
71+
8. **Check context in loops** — Always check `ctx.Done()` in long-running operations
7172

7273
## Git Configuration
7374

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ REPO_NAME := eidos
55
VERSION ?= $(shell git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
66
IMAGE_REGISTRY ?= ghcr.io/nvidia
77
IMAGE_TAG ?= latest
8-
YAML_FILES := $(shell find . -type f \( -iname "*.yml" -o -iname "*.yaml" \) ! -path "./examples/*" ! -path "./bundles/*" ! -path "./.flox/*")
8+
YAML_FILES := $(shell find . -type f \( -iname "*.yml" -o -iname "*.yaml" \) ! -path "./examples/*" ! -path "./bundles/*" ! -path "./.flox/*" ! -path "*/testdata/*")
99
COMMIT := $(shell git rev-parse HEAD)
1010
BRANCH := $(shell git rev-parse --abbrev-ref HEAD)
1111
GO_VERSION := $(shell go env GOVERSION 2>/dev/null | sed 's/go//')

pkg/build/doc.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package build implements the build command for generating OCI artifacts
16+
// from build spec files. It provides spec file parsing, declarative value
17+
// mapping, and a sequential pipeline for producing charts, apps, and
18+
// app-of-apps images compatible with the runtime controller.
19+
package build

pkg/build/spec.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package build
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"os"
21+
22+
"gopkg.in/yaml.v3"
23+
24+
"github.com/NVIDIA/eidos/pkg/errors"
25+
)
26+
27+
const (
28+
// ExpectedAPIVersion is the required apiVersion for build spec files.
29+
ExpectedAPIVersion = "eidos.nvidia.com/v1beta1"
30+
// ExpectedKind is the required kind for build spec files.
31+
ExpectedKind = "EidosRuntime"
32+
)
33+
34+
// BuildSpec represents the top-level build specification file used by the
35+
// runtime controller. It contains input configuration and output status.
36+
type BuildSpec struct {
37+
APIVersion string `yaml:"apiVersion,omitempty"`
38+
Kind string `yaml:"kind,omitempty"`
39+
Spec BuildSpecConfig `yaml:"spec"`
40+
Status BuildStatus `yaml:"status,omitempty"`
41+
}
42+
43+
// BuildSpecConfig holds the input configuration for a build operation.
44+
type BuildSpecConfig struct {
45+
Recipe string `yaml:"recipe,omitempty"`
46+
Version string `yaml:"version,omitempty"`
47+
Target string `yaml:"target,omitempty"`
48+
Registry RegistryConfig `yaml:"registry"`
49+
}
50+
51+
// RegistryConfig holds OCI registry connection details.
52+
type RegistryConfig struct {
53+
Host string `yaml:"host"`
54+
Repository string `yaml:"repository"`
55+
InsecureTLS bool `yaml:"insecureTLS,omitempty"`
56+
}
57+
58+
// BuildStatus holds the output status written back after a build.
59+
type BuildStatus struct {
60+
Images map[string]ImageStatus `yaml:"images,omitempty"`
61+
}
62+
63+
// ImageStatus describes a single OCI image produced by the build pipeline.
64+
type ImageStatus struct {
65+
Path string `yaml:"path,omitempty"`
66+
Registry string `yaml:"registry"`
67+
Repository string `yaml:"repository"`
68+
Tag string `yaml:"tag"`
69+
Digest string `yaml:"digest,omitempty"`
70+
}
71+
72+
// LoadSpec reads and parses a build spec file from disk.
73+
func LoadSpec(ctx context.Context, path string) (*BuildSpec, error) {
74+
if err := ctx.Err(); err != nil {
75+
return nil, errors.Wrap(errors.ErrCodeTimeout, "context cancelled before reading spec", err)
76+
}
77+
78+
data, err := os.ReadFile(path)
79+
if err != nil {
80+
if os.IsNotExist(err) {
81+
return nil, errors.Wrap(errors.ErrCodeNotFound,
82+
fmt.Sprintf("spec file not found: %q", path), err)
83+
}
84+
return nil, errors.Wrap(errors.ErrCodeInternal,
85+
fmt.Sprintf("failed to read spec file %q", path), err)
86+
}
87+
88+
var spec BuildSpec
89+
if err := yaml.Unmarshal(data, &spec); err != nil {
90+
return nil, errors.Wrap(errors.ErrCodeInvalidRequest,
91+
fmt.Sprintf("failed to parse spec file %q", path), err)
92+
}
93+
94+
return &spec, nil
95+
}
96+
97+
// Validate checks that required fields are present in the spec.
98+
func (s *BuildSpec) Validate() error {
99+
if s.APIVersion != ExpectedAPIVersion {
100+
return errors.New(errors.ErrCodeInvalidRequest,
101+
fmt.Sprintf("apiVersion must be %q, got %q", ExpectedAPIVersion, s.APIVersion))
102+
}
103+
104+
if s.Kind != ExpectedKind {
105+
return errors.New(errors.ErrCodeInvalidRequest,
106+
fmt.Sprintf("kind must be %q, got %q", ExpectedKind, s.Kind))
107+
}
108+
109+
if s.Spec.Registry.Host == "" {
110+
return errors.New(errors.ErrCodeInvalidRequest, "spec.registry.host is required")
111+
}
112+
113+
if s.Spec.Registry.Repository == "" {
114+
return errors.New(errors.ErrCodeInvalidRequest, "spec.registry.repository is required")
115+
}
116+
117+
return nil
118+
}
119+
120+
// WriteBack marshals the spec (including updated status) back to disk.
121+
func (s *BuildSpec) WriteBack(ctx context.Context, path string) error {
122+
if err := ctx.Err(); err != nil {
123+
return errors.Wrap(errors.ErrCodeTimeout, "context cancelled before writing spec", err)
124+
}
125+
126+
data, err := yaml.Marshal(s)
127+
if err != nil {
128+
return errors.Wrap(errors.ErrCodeInternal, "failed to marshal spec", err)
129+
}
130+
131+
if err := os.WriteFile(path, data, 0o600); err != nil {
132+
return errors.Wrap(errors.ErrCodeInternal,
133+
fmt.Sprintf("failed to write spec file %q", path), err)
134+
}
135+
136+
return nil
137+
}
138+
139+
// SetImageStatus sets the status for a named image (e.g., "charts", "apps", "app-of-apps").
140+
func (s *BuildSpec) SetImageStatus(name string, status ImageStatus) {
141+
if s.Status.Images == nil {
142+
s.Status.Images = make(map[string]ImageStatus)
143+
}
144+
s.Status.Images[name] = status
145+
}

0 commit comments

Comments
 (0)