Skip to content

Commit

Permalink
feat: adds update bundle and update deployments commands
Browse files Browse the repository at this point in the history
  • Loading branch information
jmgilman committed Jan 30, 2024
1 parent 81eed2f commit 8be0fdb
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 30 deletions.
113 changes: 93 additions & 20 deletions tools/updater/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ package main
import (
"encoding/json"
"fmt"
"io"
"os"
"path"
"strings"

"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"cuelang.org/go/cue/format"
"github.com/alecthomas/kong"
"github.com/input-output-hk/catalyst-ci/tools/updater/pkg"
ch "github.com/mheers/cue-helper/pkg/value"
"github.com/spf13/afero"
)

Expand All @@ -22,6 +20,12 @@ var cli struct {
Update updateCmd `cmd:"" help:"Overrides a target path in a CUE file with the given value."`
}

type updateCmd struct {
Bundle updateBundleCmd `cmd:"" help:"Overrides a target path in a Timoni bundle values field with the given value."`
Deployments updateDeploymentsCmd `cmd:"" help:"Performs a mass update on Timoni bundle files using given input data."`
File updateFileCmd `cmd:"" help:"Overrides a target path in a CUE file with the given value."`
}

type scanCmd struct {
Path string `arg:"" help:"The path to scan for deployment files." type:"existingdir" required:"true"`
Template []string `short:"t" help:"A key/value pair used to override constant values in deployment configurations."`
Expand Down Expand Up @@ -53,36 +57,105 @@ func (c *scanCmd) Run() error {
return nil
}

type updateCmd struct {
BundleFile string `type:"existingfile" short:"b" help:"Path to the Timoni bundle file to modify." required:"true"`
Path string `arg:"" help:"A dot separated path to the value to override (must already exist)."`
Value string `arg:"" help:"The value to override the value at the path with."`
type updateBundleCmd struct {
BundleFile string `type:"existingfile" short:"f" help:"Path to the bundle file to update." required:"true"`
Instance string `short:"i" help:"The instance to update." required:"true"`
InPlace bool `help:"Update the file in place."`
Path string `arg:"" help:"A dot separated path to the value to update (must already exist)."`
Value string `arg:"" help:"The value to updates the value at the path with."`
}

func (c *updateCmd) Run() error {
cuectx := cuecontext.New()
v, err := pkg.ReadFile(cuectx, c.BundleFile, afero.NewOsFs())
func (c *updateBundleCmd) Run() error {
path := fmt.Sprintf("bundle.instances.%s.values.%s", c.Instance, c.Path)

src, err := pkg.UpdateFile(c.BundleFile, path, c.Value, afero.NewOsFs())
if err != nil {
return err
}

if !v.LookupPath(cue.ParsePath(c.Path)).Exists() {
return fmt.Errorf("path %q does not exist", c.Path)
if c.InPlace {
if err := os.WriteFile(c.BundleFile, src, 0644); err != nil { //nolint:gosec
return fmt.Errorf("failed to write file %q: %v", c.BundleFile, err)
}
} else {
fmt.Print(string(src))
}

v, err = ch.Replace(v, c.Path, c.Value)
if err != nil {
return err
return nil
}

type updateDeploymentsCmd struct {
RootDir string `arg:"" type:"existingdir" help:"The root directory where Timoni bundle files are located." required:"true"`
Environment string `short:"e" help:"The environment to update." required:"true"`
Input string `short:"i" help:"The path to the input data file (can be passed via stdin)."`
}

func (c *updateDeploymentsCmd) Run() error {
var data []byte

_, err := os.Stat(c.Input)
if os.IsNotExist(err) {
data, err = io.ReadAll(os.Stdin)
if err != nil {
return fmt.Errorf("failed to read input data: %v", err)
}
} else {
data, err = os.ReadFile(c.Input)
if err != nil {
return fmt.Errorf("failed to read input data from %s: %v", c.Input, err)
}
}

overrides := []pkg.OverrideConfig{}
if err := json.Unmarshal(data, &overrides); err != nil {
return fmt.Errorf("failed to parse input data: %v", err)
}

for _, override := range overrides {
bundlePath := path.Join(c.RootDir, c.Environment, override.App, "bundle.cue")
if _, err := os.Stat(bundlePath); os.IsNotExist(err) {
return fmt.Errorf("bundle file %q does not exist", bundlePath)
}

var path string
if override.Instance == "" {
path = fmt.Sprintf("bundle.instances.%s.values.%s", override.App, override.Path)
} else {
path = fmt.Sprintf("bundle.instances.%s.values.%s", override.Instance, override.Path)
}

src, err := pkg.UpdateFile(bundlePath, path, override.Value, afero.NewOsFs())
if err != nil {
return fmt.Errorf("failed to update bundle file %q: %v", bundlePath, err)
}

if err := os.WriteFile(bundlePath, src, 0644); err != nil { //nolint:gosec
return fmt.Errorf("failed to write bundle file %q: %v", bundlePath, err)
}
}

node := v.Syntax(cue.Final(), cue.Concrete(true), cue.Docs(true))
src, err := format.Node(node)
return nil
}

type updateFileCmd struct {
File string `type:"existingfile" short:"f" help:"Path to the CUE file to update." required:"true"`
InPlace bool `help:"Update the file in place."`
Path string `arg:"" help:"A dot separated path to the value to update (must already exist)."`
Value string `arg:"" help:"The value to updates the value at the path with."`
}

func (c *updateFileCmd) Run() error {
src, err := pkg.UpdateFile(c.File, c.Path, c.Value, afero.NewOsFs())
if err != nil {
return err
}

if err := os.WriteFile(c.BundleFile, src, 0644); err != nil { //nolint:gosec
return fmt.Errorf("failed to write file %q: %v", c.BundleFile, err)
if c.InPlace {
if err := os.WriteFile(c.File, src, 0644); err != nil { //nolint:gosec
return fmt.Errorf("failed to write file %q: %v", c.File, err)
}
} else {
fmt.Print(string(src))
}

return nil
Expand Down
32 changes: 30 additions & 2 deletions tools/updater/pkg/cue.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import (
"fmt"

"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"cuelang.org/go/cue/format"
ch "github.com/mheers/cue-helper/pkg/value"
"github.com/spf13/afero"
)

// ReadFile reads a CUE file and returns a cue.Value.
func ReadFile(ctx *cue.Context, path string, os afero.Fs) (cue.Value, error) {
contents, err := afero.ReadFile(os, path)
func ReadFile(ctx *cue.Context, path string, fs afero.Fs) (cue.Value, error) {
contents, err := afero.ReadFile(fs, path)
if err != nil {
return cue.Value{}, fmt.Errorf("failed to read file %q: %w", path, err)
}
Expand All @@ -23,3 +26,28 @@ func ReadFile(ctx *cue.Context, path string, os afero.Fs) (cue.Value, error) {

return v, nil
}

// UpdateFile updates a CUE file at the given path with the given value and returns the updated file contents.
func UpdateFile(filePath, path string, value interface{}, fs afero.Fs) ([]byte, error) {
v, err := ReadFile(cuecontext.New(), filePath, fs)
if err != nil {
return nil, err
}

if !v.LookupPath(cue.ParsePath(path)).Exists() {
return nil, fmt.Errorf("path %q does not exist", path)
}

v, err = ch.Replace(v, path, value)
if err != nil {
return nil, fmt.Errorf("failed to replace value at path %q: %w", path, err)
}

node := v.Syntax(cue.Final(), cue.Concrete(true), cue.Docs(true))
src, err := format.Node(node)
if err != nil {
return nil, fmt.Errorf("failed to format CUE file: %w", err)
}

return src, nil
}
53 changes: 48 additions & 5 deletions tools/updater/pkg/cue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package pkg_test
// cspell: words afero cuelang cuecontext

import (
"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"github.com/input-output-hk/catalyst-ci/tools/updater/pkg"
. "github.com/onsi/ginkgo/v2"
Expand All @@ -12,19 +13,19 @@ import (

var _ = Describe("Cue", func() {
Describe("ReadFile", func() {
var os afero.Fs
var fs afero.Fs

BeforeEach(func() {
os = afero.NewMemMapFs()
fs = afero.NewMemMapFs()
})

When("the file exists", func() {
It("returns a cue.Value", func() {
err := afero.WriteFile(os, "foo.cue", []byte("foo: 1"), 0644)
err := afero.WriteFile(fs, "foo.cue", []byte("foo: 1"), 0644)
Expect(err).ToNot(HaveOccurred())

ctx := cuecontext.New()
v, err := pkg.ReadFile(ctx, "foo.cue", os)
v, err := pkg.ReadFile(ctx, "foo.cue", fs)
Expect(err).ToNot(HaveOccurred())
Expect(v).ToNot(BeNil())
})
Expand All @@ -34,9 +35,51 @@ var _ = Describe("Cue", func() {
It("returns an error", func() {
ctx := cuecontext.New()

_, err := pkg.ReadFile(ctx, "foo.cue", os)
_, err := pkg.ReadFile(ctx, "foo.cue", fs)
Expect(err).To(HaveOccurred())
})
})
})

Describe("UpdateFile", func() {
var fs afero.Fs

When("updating a file", func() {
BeforeEach(func() {
fs = afero.NewMemMapFs()
})

When("the file exists", func() {
BeforeEach(func() {
err := afero.WriteFile(fs, "foo.cue", []byte("foo: 1"), 0644)
Expect(err).ToNot(HaveOccurred())
})

When("the path exists", func() {
It("updates the file", func() {
src, err := pkg.UpdateFile("foo.cue", "foo", 2, fs)
Expect(err).ToNot(HaveOccurred())

ctx := cuecontext.New()
v := ctx.CompileBytes(src)
Expect(v.LookupPath(cue.ParsePath("foo")).Int64()).To(Equal(int64(2)))
})
})

When("the path does not exist", func() {
It("returns an error", func() {
_, err := pkg.UpdateFile("foo.cue", "bar", 2, fs)
Expect(err).To(HaveOccurred())
})
})
})

When("the file does not exist", func() {
It("returns an error", func() {
_, err := pkg.UpdateFile("foo.cue", "foo", 2, fs)
Expect(err).To(HaveOccurred())
})
})
})
})
})
7 changes: 4 additions & 3 deletions tools/updater/pkg/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ type DeploymentFile struct {

// OverrideConfig represents configuration for overriding a value in a CUE file.
type OverrideConfig struct {
App string `json:"app" yaml:"app"`
Path string `json:"path" yaml:"path"`
Value string `json:"value" yaml:"value"`
App string `json:"app" yaml:"app"`
Instance string `json:"instance" yaml:"instance"`
Path string `json:"path" yaml:"path"`
Value string `json:"value" yaml:"value"`
}

// ScanForDeploymentFiles scans a directory for deployment configuration files.
Expand Down

0 comments on commit 8be0fdb

Please sign in to comment.