Skip to content

Commit fce3d97

Browse files
authored
feat: uses common KCL module for defining deployment modules (#134)
1 parent 5a9fc46 commit fce3d97

File tree

34 files changed

+531
-102
lines changed

34 files changed

+531
-102
lines changed

blueprint.cue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ global: {
5555
}
5656
registry: "ghcr.io"
5757
}
58+
59+
kcl: {
60+
install: true
61+
registries: [
62+
"ghcr.io/input-output-hk/catalyst-forge",
63+
]
64+
version: "v0.11.0"
65+
}
5866
}
5967
secrets: [
6068
{

cli/out.cue

Lines changed: 0 additions & 34 deletions
This file was deleted.

foundry/api/blueprint.cue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ project: {
2121
modules: {
2222
main: {
2323
name: "app"
24-
version: "0.2.0"
24+
version: "0.4.0"
2525
values: {
2626
deployment: containers: main: {
2727
image: {

lib/project/deployment/deployer/deployer_test.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/input-output-hk/catalyst-forge/lib/project/schema"
1919
"github.com/input-output-hk/catalyst-forge/lib/project/secrets"
2020
sm "github.com/input-output-hk/catalyst-forge/lib/project/secrets/mocks"
21+
"github.com/input-output-hk/catalyst-forge/lib/project/utils"
2122
rm "github.com/input-output-hk/catalyst-forge/lib/tools/git/repo/remote/mocks"
2223
"github.com/input-output-hk/catalyst-forge/lib/tools/testutils"
2324
"github.com/spf13/afero"
@@ -81,11 +82,11 @@ func TestDeployerDeploy(t *testing.T) {
8182
schema.DeploymentModuleBundle{
8283
"main": {
8384
Instance: "instance",
84-
Name: "module",
85+
Name: utils.StringPtr("module"),
8586
Namespace: "default",
86-
Registry: "registry",
87+
Registry: utils.StringPtr("registry"),
8788
Values: map[string]string{"key": "value"},
88-
Version: "v1.0.0",
89+
Version: utils.StringPtr("v1.0.0"),
8990
},
9091
},
9192
),
@@ -144,11 +145,11 @@ func TestDeployerDeploy(t *testing.T) {
144145
schema.DeploymentModuleBundle{
145146
"main": {
146147
Instance: "instance",
147-
Name: "module",
148+
Name: utils.StringPtr("module"),
148149
Namespace: "default",
149-
Registry: "registry",
150+
Registry: utils.StringPtr("registry"),
150151
Values: map[string]string{"key": "value"},
151-
Version: "v1.0.0",
152+
Version: utils.StringPtr("v1.0.0"),
152153
},
153154
},
154155
),

lib/project/deployment/generator/generator.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ func (d *Generator) GenerateBundle(b schema.DeploymentModuleBundle) (GeneratorRe
4747

4848
// Generate generates manifests for a deployment module.
4949
func (d *Generator) Generate(m schema.DeploymentModule) ([]byte, error) {
50+
if err := deployment.Validate(m); err != nil {
51+
return nil, fmt.Errorf("failed to validate module: %w", err)
52+
}
53+
5054
manifests, err := d.mg.Generate(m)
5155
if err != nil {
5256
return nil, fmt.Errorf("failed to generate manifest for module: %w", err)

lib/project/deployment/generator/generator_test.go

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"cuelang.org/go/cue/cuecontext"
88
"github.com/input-output-hk/catalyst-forge/lib/project/deployment/mocks"
99
"github.com/input-output-hk/catalyst-forge/lib/project/schema"
10+
"github.com/input-output-hk/catalyst-forge/lib/project/utils"
1011
"github.com/input-output-hk/catalyst-forge/lib/tools/testutils"
1112
"github.com/stretchr/testify/assert"
1213
"github.com/stretchr/testify/require"
@@ -26,11 +27,11 @@ func TestGeneratorGenerateBundle(t *testing.T) {
2627
bundle: schema.DeploymentModuleBundle{
2728
"test": schema.DeploymentModule{
2829
Instance: "instance",
29-
Name: "test",
30+
Name: utils.StringPtr("test"),
3031
Namespace: "default",
31-
Registry: "registry",
32+
Registry: utils.StringPtr("registry"),
3233
Values: ctx.CompileString(`foo: "bar"`),
33-
Version: "1.0.0",
34+
Version: utils.StringPtr("1.0.0"),
3435
},
3536
},
3637
yaml: "test",
@@ -59,11 +60,11 @@ func TestGeneratorGenerateBundle(t *testing.T) {
5960
bundle: schema.DeploymentModuleBundle{
6061
"test": schema.DeploymentModule{
6162
Instance: "instance",
62-
Name: "test",
63+
Name: utils.StringPtr("test"),
6364
Namespace: "default",
64-
Registry: "registry",
65+
Registry: utils.StringPtr("registry"),
6566
Values: ctx.CompileString(`foo: "bar"`),
66-
Version: "1.0.0",
67+
Version: utils.StringPtr("1.0.0"),
6768
},
6869
},
6970
yaml: "test",
@@ -77,11 +78,11 @@ func TestGeneratorGenerateBundle(t *testing.T) {
7778
bundle: schema.DeploymentModuleBundle{
7879
"test": schema.DeploymentModule{
7980
Instance: "instance",
80-
Name: "test",
81+
Name: utils.StringPtr("test"),
8182
Namespace: "default",
82-
Registry: "registry",
83+
Registry: utils.StringPtr("registry"),
8384
Values: fmt.Errorf("error"),
84-
Version: "1.0.0",
85+
Version: utils.StringPtr("1.0.0"),
8586
},
8687
},
8788
yaml: "test",
@@ -127,11 +128,11 @@ func TestGeneratorGenerate(t *testing.T) {
127128
name: "full",
128129
module: schema.DeploymentModule{
129130
Instance: "instance",
130-
Name: "test",
131+
Name: utils.StringPtr("test"),
131132
Namespace: "default",
132-
Registry: "registry",
133+
Registry: utils.StringPtr("registry"),
133134
Values: ctx.CompileString(`foo: "bar"`),
134-
Version: "1.0.0",
135+
Version: utils.StringPtr("1.0.0"),
135136
},
136137
yaml: "test",
137138
err: false,
@@ -143,10 +144,10 @@ func TestGeneratorGenerate(t *testing.T) {
143144
{
144145
name: "manifest error",
145146
module: schema.DeploymentModule{
146-
Name: "test",
147+
Name: utils.StringPtr("test"),
147148
Namespace: "default",
148149
Values: ctx.CompileString(`foo: "bar"`),
149-
Version: "1.0.0",
150+
Version: utils.StringPtr("1.0.0"),
150151
},
151152
yaml: "test",
152153
err: true,

lib/project/deployment/module.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,14 @@ func ParseBundle(src []byte) (schema.DeploymentModuleBundle, error) {
5757

5858
return bundle, nil
5959
}
60+
61+
// Validate validates a deployment module.
62+
func Validate(mod schema.DeploymentModule) error {
63+
if mod.Path == nil {
64+
if mod.Name == nil || mod.Registry == nil || mod.Version == nil {
65+
return fmt.Errorf("module must have at least one of (name, registry, version) or path")
66+
}
67+
}
68+
69+
return nil
70+
}

lib/project/deployment/providers/kcl/client/config.go

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,27 @@ import (
88

99
// KCLModuleConfig contains the configuration given to a KCL module.
1010
type KCLModuleConfig struct {
11-
InstanceName string
12-
Namespace string
13-
Values any
11+
Instance string `json:"instance"`
12+
Name string `json:"name"`
13+
Namespace string `json:"namespace"`
14+
Values any `json:"values"`
15+
Version string `json:"version"`
1416
}
1517

1618
func (k *KCLModuleConfig) ToArgs() ([]string, error) {
1719
ctx := cuecontext.New()
18-
v := ctx.Encode(k.Values)
20+
v := ctx.Encode(k)
1921
if v.Err() != nil {
20-
return nil, fmt.Errorf("failed to encode values: %w", v.Err())
22+
return nil, fmt.Errorf("failed to encode module: %w", v.Err())
2123
}
2224

2325
j, err := v.MarshalJSON()
2426
if err != nil {
25-
return nil, fmt.Errorf("failed to marshal values to JSON: %w", err)
27+
return nil, fmt.Errorf("failed to marshal module to JSON: %w", err)
2628
}
2729

2830
return []string{
2931
"-D",
30-
fmt.Sprintf("name=%s", k.InstanceName),
31-
"-D",
32-
fmt.Sprintf("namespace=%s", k.Namespace),
33-
"-D",
34-
fmt.Sprintf("values=%s", string(j)),
32+
fmt.Sprintf("deployment=%s", string(j)),
3533
}, nil
3634
}

lib/project/deployment/providers/kcl/client/kpm.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@ package client
33
import (
44
"bytes"
55
"fmt"
6+
"strings"
67

78
kpm "kcl-lang.io/kpm/pkg/client"
9+
"kcl-lang.io/kpm/pkg/downloader"
810
)
911

1012
// KPMClient is a KCLClient that uses the KPM Go package to run modules.
1113
type KPMClient struct {
1214
logOutput bytes.Buffer
1315
}
1416

15-
func (k KPMClient) Run(container string, conf KCLModuleConfig) (string, error) {
17+
func (k KPMClient) Run(path string, conf KCLModuleConfig) (string, error) {
1618
client, err := kpm.NewKpmClient()
1719
if err != nil {
1820
return "", fmt.Errorf("failed to create KPM client: %w", err)
@@ -24,10 +26,22 @@ func (k KPMClient) Run(container string, conf KCLModuleConfig) (string, error) {
2426
return "", fmt.Errorf("failed to generate KCL arguments: %w", err)
2527
}
2628

27-
out, err := client.Run(
28-
kpm.WithRunSourceUrl(container),
29+
runArgs := []kpm.RunOption{
2930
kpm.WithArguments(args),
30-
)
31+
}
32+
33+
if strings.HasPrefix(path, "oci://") {
34+
runArgs = append(runArgs, kpm.WithRunSourceUrl(path))
35+
} else {
36+
src, err := downloader.NewSourceFromStr(path)
37+
if err != nil {
38+
return "", fmt.Errorf("failed to create KCL source: %w", err)
39+
}
40+
41+
runArgs = append(runArgs, kpm.WithRunSource(src))
42+
}
43+
44+
out, err := client.Run(runArgs...)
3145

3246
if err != nil {
3347
return "", fmt.Errorf("failed to run KCL module: %w", err)

lib/project/deployment/providers/kcl/generator.go

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,95 @@ import (
44
"fmt"
55
"io"
66
"log/slog"
7+
"path/filepath"
78
"strings"
89

10+
"github.com/BurntSushi/toml"
911
"github.com/input-output-hk/catalyst-forge/lib/project/deployment/providers/kcl/client"
1012
"github.com/input-output-hk/catalyst-forge/lib/project/schema"
13+
"github.com/spf13/afero"
1114
)
1215

16+
// KCLModule represents a KCL module.
17+
type KCLModule struct {
18+
Package KCLModulePackage `toml:"package"`
19+
}
20+
21+
// KCLModulePackage represents a KCL module package.
22+
type KCLModulePackage struct {
23+
Name string `toml:"name"`
24+
Edition string `toml:"edition"`
25+
Version string `toml:"version"`
26+
}
27+
1328
// KCLManifestGenerator is a ManifestGenerator that uses KCL.
1429
type KCLManifestGenerator struct {
1530
client client.KCLClient
31+
fs afero.Fs
1632
logger *slog.Logger
1733
}
1834

1935
func (g *KCLManifestGenerator) Generate(mod schema.DeploymentModule) ([]byte, error) {
20-
container := fmt.Sprintf("oci://%s/%s?tag=%s", strings.TrimSuffix(mod.Registry, "/"), mod.Name, mod.Version)
21-
conf := client.KCLModuleConfig{
22-
InstanceName: mod.Instance,
23-
Namespace: mod.Namespace,
24-
Values: mod.Values,
36+
var conf client.KCLModuleConfig
37+
var path string
38+
if mod.Path != nil {
39+
g.logger.Info("Parsing local KCL module", "path", *mod.Path)
40+
kmod, err := g.parseModule(*mod.Path)
41+
if err != nil {
42+
return nil, fmt.Errorf("failed to parse KCL module: %w", err)
43+
}
44+
45+
path = *mod.Path
46+
conf = client.KCLModuleConfig{
47+
Instance: mod.Instance,
48+
Name: kmod.Package.Name,
49+
Namespace: mod.Namespace,
50+
Values: mod.Values,
51+
Version: kmod.Package.Version,
52+
}
53+
} else {
54+
path = fmt.Sprintf("oci://%s/%s?tag=%s", strings.TrimSuffix(*mod.Registry, "/"), *mod.Name, *mod.Version)
55+
conf = client.KCLModuleConfig{
56+
Instance: mod.Instance,
57+
Name: *mod.Name,
58+
Namespace: mod.Namespace,
59+
Values: mod.Values,
60+
Version: *mod.Version,
61+
}
2562
}
2663

27-
out, err := g.client.Run(container, conf)
64+
out, err := g.client.Run(path, conf)
2865
if err != nil {
2966
return nil, fmt.Errorf("failed to run KCL module: %w", err)
3067
}
3168

3269
return []byte(out), nil
3370
}
3471

72+
// parseModule parses a KCL module from the given path.
73+
func (g *KCLManifestGenerator) parseModule(path string) (KCLModule, error) {
74+
modPath := filepath.Join(path, "kcl.mod")
75+
exists, err := afero.Exists(g.fs, modPath)
76+
if err != nil {
77+
return KCLModule{}, fmt.Errorf("failed to check if KCL module exists: %w", err)
78+
} else if !exists {
79+
return KCLModule{}, fmt.Errorf("KCL module not found")
80+
}
81+
82+
src, err := afero.ReadFile(g.fs, modPath)
83+
if err != nil {
84+
return KCLModule{}, fmt.Errorf("failed to read KCL module: %w", err)
85+
}
86+
87+
var mod KCLModule
88+
_, err = toml.Decode(string(src), &mod)
89+
if err != nil {
90+
return KCLModule{}, fmt.Errorf("failed to decode KCL module: %w", err)
91+
}
92+
93+
return mod, nil
94+
}
95+
3596
// NewKCLManifestGenerator creates a new KCL manifest generator.
3697
func NewKCLManifestGenerator(logger *slog.Logger) *KCLManifestGenerator {
3798
if logger == nil {
@@ -40,6 +101,7 @@ func NewKCLManifestGenerator(logger *slog.Logger) *KCLManifestGenerator {
40101

41102
return &KCLManifestGenerator{
42103
client: client.KPMClient{},
104+
fs: afero.NewOsFs(),
43105
logger: logger,
44106
}
45107
}

0 commit comments

Comments
 (0)