Skip to content

Commit

Permalink
refactor(cli): refactors deployment commands to use KCL instead of Ti…
Browse files Browse the repository at this point in the history
…moni
  • Loading branch information
jmgilman committed Jan 17, 2025
1 parent aa2f9e4 commit da728db
Show file tree
Hide file tree
Showing 8 changed files with 437 additions and 19 deletions.
2 changes: 1 addition & 1 deletion blueprint.cue
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ global: {
]
}
deployment: {
registry: ci.providers.aws.ecr.registry
registry: ci.providers.aws.ecr.registry + "/catalyst-deployments"
repo: {
url: "https://github.com/input-output-hk/catalyst-world"
ref: "master"
Expand Down
24 changes: 13 additions & 11 deletions cli/cmd/cmds/deploy/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

type TemplateCmd struct {
Project string `arg:"" help:"The path to the project." kong:"arg,predictor=path"`
Values bool `help:"Only print the values.yml for the main module"`
}

func (c *TemplateCmd) Run(ctx run.RunContext) error {
Expand All @@ -17,22 +18,23 @@ func (c *TemplateCmd) Run(ctx run.RunContext) error {
return fmt.Errorf("could not load project: %w", err)
}

bundle, err := deployment.GenerateBundle(&project)
if err != nil {
return fmt.Errorf("could not generate bundle: %w", err)
}
runner := deployment.NewKCLRunner(ctx.Logger)

templater, err := deployment.NewDefaultBundleTemplater(ctx.Logger)
if err != nil {
return fmt.Errorf("could not create bundle templater: %w", err)
if c.Values {
values, err := runner.GetMainValues(&project)
if err != nil {
return fmt.Errorf("could not get values: %w", err)
}

fmt.Print(values)
return nil
}

out, err := templater.Render(bundle)
out, err := runner.RunDeployment(&project)
if err != nil {
return fmt.Errorf("could not render bundle: %w", err)
return fmt.Errorf("could not run deployment: %w", err)
}

fmt.Println(out)

fmt.Print(out)
return nil
}
171 changes: 171 additions & 0 deletions cli/pkg/deployment/kcl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package deployment

import (
"encoding/json"
"fmt"
"io"
"log/slog"
"strings"

"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"github.com/input-output-hk/catalyst-forge/cli/pkg/executor"
"github.com/input-output-hk/catalyst-forge/lib/project/project"
"github.com/input-output-hk/catalyst-forge/lib/project/schema"
"gopkg.in/yaml.v3"
)

// KCLModuleArgs contains the arguments to pass to the KCL module.
type KCLModuleArgs struct {
// InstanceName is the name to use for the deployment instance.
InstanceName string

// Namespace is the namespace to deploy the module to.
Namespace string

// Values contains the values to pass to the module.
Values string

// Version is the version of the module to deploy.
Version string
}

// Serialize serializes the KCLModuleArgs to a list of arguments.
func (k *KCLModuleArgs) Serialize() []string {
return []string{
"-D",
fmt.Sprintf("name=%s", k.InstanceName),
"-D",
fmt.Sprintf("namespace=%s", k.Namespace),
"-D",
fmt.Sprintf("values=%s", k.Values),
"-D",
k.Version,
}
}

// KCLRunner is used to run KCL commands.
type KCLRunner struct {
kcl executor.WrappedExecuter
logger *slog.Logger
}

// GetMainValues returns the values (in YAML) for the main module in the project.
func (k *KCLRunner) GetMainValues(p *project.Project) (string, error) {
if p.Blueprint.Project.Deployment.Modules == nil {
return "", fmt.Errorf("no deployment modules found in project blueprint")
} else if p.Blueprint.Global.Deployment.Registry == "" {
return "", fmt.Errorf("no deployment registry found in project blueprint")
}

ctx := cuecontext.New()
module := p.Blueprint.Project.Deployment.Modules.Main

json, err := encodeValues(ctx, module)
if err != nil {
return "", fmt.Errorf("failed to encode module values: %w", err)
}

yaml, err := jsonToYaml(json)
if err != nil {
return "", fmt.Errorf("failed to convert values to YAML: %w", err)
}

return string(yaml), nil
}

// RunDeployment runs the deployment modules in the project and returns the
// combined output.
func (k *KCLRunner) RunDeployment(p *project.Project) (string, error) {
ctx := cuecontext.New()
if p.Blueprint.Project.Deployment.Modules == nil {
return "", fmt.Errorf("no deployment modules found in project blueprint")
} else if p.Blueprint.Global.Deployment.Registry == "" {
return "", fmt.Errorf("no deployment registry found in project blueprint")
}

modules := map[string]schema.Module{"main": p.Blueprint.Project.Deployment.Modules.Main}
for k, v := range p.Blueprint.Project.Deployment.Modules.Support {
modules[k] = v
}

var final string
for _, module := range modules {
json, err := encodeValues(ctx, module)
if err != nil {
return "", fmt.Errorf("failed to encode module values: %w", err)
}

args := KCLModuleArgs{
InstanceName: p.Blueprint.Project.Name,
Namespace: module.Namespace,
Values: string(json),
Version: module.Version,
}

container := fmt.Sprintf("%s/%s", strings.TrimSuffix(p.Blueprint.Global.Deployment.Registry, "/"), module.Module)
out, err := k.run(container, args)
if err != nil {
k.logger.Error("Failed to run KCL module", "module", module.Module, "error", err, "output", string(out))
return "", fmt.Errorf("failed to run KCL module: %w", err)
}

final += strings.Trim(string(out), "\n") + "\n---\n"
}

return strings.TrimSuffix(final, "---\n"), nil
}

// encodeValues encodes the values of a module to JSON.
func encodeValues(ctx *cue.Context, module schema.Module) ([]byte, error) {
v := ctx.Encode(module.Values)
if err := v.Validate(); err != nil {
return nil, fmt.Errorf("failed to validate module values: %w", err)
}

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

return j, nil
}

// run runs a KCL module with the given module container and arguments.
func (k *KCLRunner) run(container string, moduleArgs KCLModuleArgs) ([]byte, error) {
args := []string{"run", "-q"}
args = append(args, moduleArgs.Serialize()...)
args = append(args, fmt.Sprintf("oci://%s", container))

k.logger.Debug("Running KCL module", "container", container, "args", args)
return k.kcl.Execute(args...)
}

// jsonToYaml converts a JSON string to a YAML string.
func jsonToYaml(j []byte) ([]byte, error) {
var jsonObject map[string]interface{}
err := json.Unmarshal(j, &jsonObject)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
}

y, err := yaml.Marshal(jsonObject)
if err != nil {
return nil, fmt.Errorf("failed to marshal YAML: %w", err)
}

return y, nil
}

// NewKCLRunner creates a new KCLRunner.
func NewKCLRunner(logger *slog.Logger) KCLRunner {
if logger == nil {
logger = slog.New(slog.NewTextHandler(io.Discard, nil))
}

kcl := executor.NewLocalWrappedExecutor(executor.NewLocalExecutor(logger), "kcl")
return KCLRunner{
kcl: kcl,
logger: logger,
}
}
Loading

0 comments on commit da728db

Please sign in to comment.