Skip to content

Commit

Permalink
feat: uses common KCL module for defining deployment modules (#134)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmgilman authored Feb 5, 2025
1 parent 5a9fc46 commit fce3d97
Show file tree
Hide file tree
Showing 34 changed files with 531 additions and 102 deletions.
8 changes: 8 additions & 0 deletions blueprint.cue
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ global: {
}
registry: "ghcr.io"
}

kcl: {
install: true
registries: [
"ghcr.io/input-output-hk/catalyst-forge",
]
version: "v0.11.0"
}
}
secrets: [
{
Expand Down
34 changes: 0 additions & 34 deletions cli/out.cue

This file was deleted.

2 changes: 1 addition & 1 deletion foundry/api/blueprint.cue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ project: {
modules: {
main: {
name: "app"
version: "0.2.0"
version: "0.4.0"
values: {
deployment: containers: main: {
image: {
Expand Down
13 changes: 7 additions & 6 deletions lib/project/deployment/deployer/deployer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/input-output-hk/catalyst-forge/lib/project/schema"
"github.com/input-output-hk/catalyst-forge/lib/project/secrets"
sm "github.com/input-output-hk/catalyst-forge/lib/project/secrets/mocks"
"github.com/input-output-hk/catalyst-forge/lib/project/utils"
rm "github.com/input-output-hk/catalyst-forge/lib/tools/git/repo/remote/mocks"
"github.com/input-output-hk/catalyst-forge/lib/tools/testutils"
"github.com/spf13/afero"
Expand Down Expand Up @@ -81,11 +82,11 @@ func TestDeployerDeploy(t *testing.T) {
schema.DeploymentModuleBundle{
"main": {
Instance: "instance",
Name: "module",
Name: utils.StringPtr("module"),
Namespace: "default",
Registry: "registry",
Registry: utils.StringPtr("registry"),
Values: map[string]string{"key": "value"},
Version: "v1.0.0",
Version: utils.StringPtr("v1.0.0"),
},
},
),
Expand Down Expand Up @@ -144,11 +145,11 @@ func TestDeployerDeploy(t *testing.T) {
schema.DeploymentModuleBundle{
"main": {
Instance: "instance",
Name: "module",
Name: utils.StringPtr("module"),
Namespace: "default",
Registry: "registry",
Registry: utils.StringPtr("registry"),
Values: map[string]string{"key": "value"},
Version: "v1.0.0",
Version: utils.StringPtr("v1.0.0"),
},
},
),
Expand Down
4 changes: 4 additions & 0 deletions lib/project/deployment/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ func (d *Generator) GenerateBundle(b schema.DeploymentModuleBundle) (GeneratorRe

// Generate generates manifests for a deployment module.
func (d *Generator) Generate(m schema.DeploymentModule) ([]byte, error) {
if err := deployment.Validate(m); err != nil {
return nil, fmt.Errorf("failed to validate module: %w", err)
}

manifests, err := d.mg.Generate(m)
if err != nil {
return nil, fmt.Errorf("failed to generate manifest for module: %w", err)
Expand Down
29 changes: 15 additions & 14 deletions lib/project/deployment/generator/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"cuelang.org/go/cue/cuecontext"
"github.com/input-output-hk/catalyst-forge/lib/project/deployment/mocks"
"github.com/input-output-hk/catalyst-forge/lib/project/schema"
"github.com/input-output-hk/catalyst-forge/lib/project/utils"
"github.com/input-output-hk/catalyst-forge/lib/tools/testutils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand All @@ -26,11 +27,11 @@ func TestGeneratorGenerateBundle(t *testing.T) {
bundle: schema.DeploymentModuleBundle{
"test": schema.DeploymentModule{
Instance: "instance",
Name: "test",
Name: utils.StringPtr("test"),
Namespace: "default",
Registry: "registry",
Registry: utils.StringPtr("registry"),
Values: ctx.CompileString(`foo: "bar"`),
Version: "1.0.0",
Version: utils.StringPtr("1.0.0"),
},
},
yaml: "test",
Expand Down Expand Up @@ -59,11 +60,11 @@ func TestGeneratorGenerateBundle(t *testing.T) {
bundle: schema.DeploymentModuleBundle{
"test": schema.DeploymentModule{
Instance: "instance",
Name: "test",
Name: utils.StringPtr("test"),
Namespace: "default",
Registry: "registry",
Registry: utils.StringPtr("registry"),
Values: ctx.CompileString(`foo: "bar"`),
Version: "1.0.0",
Version: utils.StringPtr("1.0.0"),
},
},
yaml: "test",
Expand All @@ -77,11 +78,11 @@ func TestGeneratorGenerateBundle(t *testing.T) {
bundle: schema.DeploymentModuleBundle{
"test": schema.DeploymentModule{
Instance: "instance",
Name: "test",
Name: utils.StringPtr("test"),
Namespace: "default",
Registry: "registry",
Registry: utils.StringPtr("registry"),
Values: fmt.Errorf("error"),
Version: "1.0.0",
Version: utils.StringPtr("1.0.0"),
},
},
yaml: "test",
Expand Down Expand Up @@ -127,11 +128,11 @@ func TestGeneratorGenerate(t *testing.T) {
name: "full",
module: schema.DeploymentModule{
Instance: "instance",
Name: "test",
Name: utils.StringPtr("test"),
Namespace: "default",
Registry: "registry",
Registry: utils.StringPtr("registry"),
Values: ctx.CompileString(`foo: "bar"`),
Version: "1.0.0",
Version: utils.StringPtr("1.0.0"),
},
yaml: "test",
err: false,
Expand All @@ -143,10 +144,10 @@ func TestGeneratorGenerate(t *testing.T) {
{
name: "manifest error",
module: schema.DeploymentModule{
Name: "test",
Name: utils.StringPtr("test"),
Namespace: "default",
Values: ctx.CompileString(`foo: "bar"`),
Version: "1.0.0",
Version: utils.StringPtr("1.0.0"),
},
yaml: "test",
err: true,
Expand Down
11 changes: 11 additions & 0 deletions lib/project/deployment/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,14 @@ func ParseBundle(src []byte) (schema.DeploymentModuleBundle, error) {

return bundle, nil
}

// Validate validates a deployment module.
func Validate(mod schema.DeploymentModule) error {
if mod.Path == nil {
if mod.Name == nil || mod.Registry == nil || mod.Version == nil {
return fmt.Errorf("module must have at least one of (name, registry, version) or path")
}
}

return nil
}
20 changes: 9 additions & 11 deletions lib/project/deployment/providers/kcl/client/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,27 @@ import (

// KCLModuleConfig contains the configuration given to a KCL module.
type KCLModuleConfig struct {
InstanceName string
Namespace string
Values any
Instance string `json:"instance"`
Name string `json:"name"`
Namespace string `json:"namespace"`
Values any `json:"values"`
Version string `json:"version"`
}

func (k *KCLModuleConfig) ToArgs() ([]string, error) {
ctx := cuecontext.New()
v := ctx.Encode(k.Values)
v := ctx.Encode(k)
if v.Err() != nil {
return nil, fmt.Errorf("failed to encode values: %w", v.Err())
return nil, fmt.Errorf("failed to encode module: %w", v.Err())
}

j, err := v.MarshalJSON()
if err != nil {
return nil, fmt.Errorf("failed to marshal values to JSON: %w", err)
return nil, fmt.Errorf("failed to marshal module to JSON: %w", err)
}

return []string{
"-D",
fmt.Sprintf("name=%s", k.InstanceName),
"-D",
fmt.Sprintf("namespace=%s", k.Namespace),
"-D",
fmt.Sprintf("values=%s", string(j)),
fmt.Sprintf("deployment=%s", string(j)),
}, nil
}
22 changes: 18 additions & 4 deletions lib/project/deployment/providers/kcl/client/kpm.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ package client
import (
"bytes"
"fmt"
"strings"

kpm "kcl-lang.io/kpm/pkg/client"
"kcl-lang.io/kpm/pkg/downloader"
)

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

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

out, err := client.Run(
kpm.WithRunSourceUrl(container),
runArgs := []kpm.RunOption{
kpm.WithArguments(args),
)
}

if strings.HasPrefix(path, "oci://") {
runArgs = append(runArgs, kpm.WithRunSourceUrl(path))
} else {
src, err := downloader.NewSourceFromStr(path)
if err != nil {
return "", fmt.Errorf("failed to create KCL source: %w", err)
}

runArgs = append(runArgs, kpm.WithRunSource(src))
}

out, err := client.Run(runArgs...)

if err != nil {
return "", fmt.Errorf("failed to run KCL module: %w", err)
Expand Down
74 changes: 68 additions & 6 deletions lib/project/deployment/providers/kcl/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,95 @@ import (
"fmt"
"io"
"log/slog"
"path/filepath"
"strings"

"github.com/BurntSushi/toml"
"github.com/input-output-hk/catalyst-forge/lib/project/deployment/providers/kcl/client"
"github.com/input-output-hk/catalyst-forge/lib/project/schema"
"github.com/spf13/afero"
)

// KCLModule represents a KCL module.
type KCLModule struct {
Package KCLModulePackage `toml:"package"`
}

// KCLModulePackage represents a KCL module package.
type KCLModulePackage struct {
Name string `toml:"name"`
Edition string `toml:"edition"`
Version string `toml:"version"`
}

// KCLManifestGenerator is a ManifestGenerator that uses KCL.
type KCLManifestGenerator struct {
client client.KCLClient
fs afero.Fs
logger *slog.Logger
}

func (g *KCLManifestGenerator) Generate(mod schema.DeploymentModule) ([]byte, error) {
container := fmt.Sprintf("oci://%s/%s?tag=%s", strings.TrimSuffix(mod.Registry, "/"), mod.Name, mod.Version)
conf := client.KCLModuleConfig{
InstanceName: mod.Instance,
Namespace: mod.Namespace,
Values: mod.Values,
var conf client.KCLModuleConfig
var path string
if mod.Path != nil {
g.logger.Info("Parsing local KCL module", "path", *mod.Path)
kmod, err := g.parseModule(*mod.Path)
if err != nil {
return nil, fmt.Errorf("failed to parse KCL module: %w", err)
}

path = *mod.Path
conf = client.KCLModuleConfig{
Instance: mod.Instance,
Name: kmod.Package.Name,
Namespace: mod.Namespace,
Values: mod.Values,
Version: kmod.Package.Version,
}
} else {
path = fmt.Sprintf("oci://%s/%s?tag=%s", strings.TrimSuffix(*mod.Registry, "/"), *mod.Name, *mod.Version)
conf = client.KCLModuleConfig{
Instance: mod.Instance,
Name: *mod.Name,
Namespace: mod.Namespace,
Values: mod.Values,
Version: *mod.Version,
}
}

out, err := g.client.Run(container, conf)
out, err := g.client.Run(path, conf)
if err != nil {
return nil, fmt.Errorf("failed to run KCL module: %w", err)
}

return []byte(out), nil
}

// parseModule parses a KCL module from the given path.
func (g *KCLManifestGenerator) parseModule(path string) (KCLModule, error) {
modPath := filepath.Join(path, "kcl.mod")
exists, err := afero.Exists(g.fs, modPath)
if err != nil {
return KCLModule{}, fmt.Errorf("failed to check if KCL module exists: %w", err)
} else if !exists {
return KCLModule{}, fmt.Errorf("KCL module not found")
}

src, err := afero.ReadFile(g.fs, modPath)
if err != nil {
return KCLModule{}, fmt.Errorf("failed to read KCL module: %w", err)
}

var mod KCLModule
_, err = toml.Decode(string(src), &mod)
if err != nil {
return KCLModule{}, fmt.Errorf("failed to decode KCL module: %w", err)
}

return mod, nil
}

// NewKCLManifestGenerator creates a new KCL manifest generator.
func NewKCLManifestGenerator(logger *slog.Logger) *KCLManifestGenerator {
if logger == nil {
Expand All @@ -40,6 +101,7 @@ func NewKCLManifestGenerator(logger *slog.Logger) *KCLManifestGenerator {

return &KCLManifestGenerator{
client: client.KPMClient{},
fs: afero.NewOsFs(),
logger: logger,
}
}
Loading

0 comments on commit fce3d97

Please sign in to comment.