Skip to content

Commit fd166ff

Browse files
authored
feat: adds updater CLI tool (#165)
1 parent 7060cbc commit fd166ff

File tree

9 files changed

+391
-0
lines changed

9 files changed

+391
-0
lines changed

.config/dictionaries/project.dic

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ subproject
5353
subprojects
5454
superfences
5555
testpackage
56+
Timoni
5657
transpiling
5758
UDCs
5859
uniseg

tools/updater/Earthfile

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
VERSION 0.7
2+
FROM golang:1.20-alpine3.18
3+
4+
# cspell: words onsi ldflags extldflags
5+
6+
fmt:
7+
DO ../../earthly/go+FMT --src="go.mod go.sum cmd pkg"
8+
9+
lint:
10+
DO ../../earthly/go+LINT --src="go.mod go.sum cmd pkg"
11+
12+
deps:
13+
WORKDIR /work
14+
15+
RUN apk add --no-cache gcc musl-dev
16+
DO ../../earthly/go+DEPS
17+
18+
src:
19+
FROM +deps
20+
21+
COPY --dir cmd pkg .
22+
23+
check:
24+
FROM +src
25+
26+
BUILD +fmt
27+
BUILD +lint
28+
29+
build:
30+
FROM +src
31+
32+
ENV CGO_ENABLED=0
33+
RUN go build -ldflags="-extldflags=-static" -o bin/updater cmd/main.go
34+
35+
SAVE ARTIFACT bin/updater updater
36+
37+
test:
38+
FROM +build
39+
40+
RUN ginkgo ./...
41+
42+
release:
43+
FROM +build
44+
45+
SAVE ARTIFACT bin/updater updater
46+
47+
publish:
48+
FROM debian:bookworm-slim
49+
WORKDIR /workspace
50+
ARG tag=latest
51+
52+
COPY +build/updater /usr/local/bin/updater
53+
54+
ENTRYPOINT ["/usr/local/bin/updater"]
55+
SAVE IMAGE --push ci-updater:${tag}

tools/updater/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# updater
2+
3+
> A helper tool for modifying CUE files to override arbitrary values.
4+
> Useful for updating Timoni bundles.
5+
6+
The `updater` CLI provides an interface for overriding existing concrete values in a given CUE file.
7+
Normally, concrete values in CUE files are immutable and thus not possible to override using the CUE CLI.
8+
However, in some cases, it may be desirable to override an existing concrete value.
9+
This is especially true in GitOps scenarios where a source of truth needs to be updated.
10+
11+
## Usage
12+
13+
The `updater` CLI is most commonly used to update Timoni bundle image tags.
14+
Assuming you have a `bundle.cue` file like this:
15+
16+
```cue
17+
bundle: {
18+
apiVersion: "v1alpha1"
19+
name: "bundle"
20+
instances: {
21+
instance: {
22+
module: {
23+
url: "oci://332405224602.dkr.ecr.eu-central-1.amazonaws.com/instance-deployment"
24+
version: "0.0.1"
25+
}
26+
namespace: "default"
27+
values: {
28+
server: image: tag: "ed2951cf049e779bba8d97413653bb06d4c28144"
29+
}
30+
}
31+
}
32+
}
33+
```
34+
35+
You can update the value of `server.image.tag` like so:
36+
37+
```shell
38+
updater -b bundle.cue "bundle.instances.instance.values.server.image.tag" "0fe74bf77739a3ef78de5fcc81c5c7a8dcae6199"
39+
```
40+
41+
The `updater` CLI will overwrite the image tag with the provided one and update the `bundle.cue` file in place.
42+
Note that the CLI uses the CUE API underneath the hood which may format the existing CUE syntax slightly differently.
43+
In some cases, the resulting syntax might be a bit unsightly, so it's recommended to run `cue fmt` on the file after processing.

tools/updater/cmd/main.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package main
2+
3+
// cspell: words alecthomas cuelang cuecontext cuectx existingfile Timoni nolint
4+
5+
import (
6+
"os"
7+
8+
"cuelang.org/go/cue"
9+
"cuelang.org/go/cue/cuecontext"
10+
"cuelang.org/go/cue/format"
11+
"github.com/alecthomas/kong"
12+
"github.com/input-output-hk/catalyst-ci/tools/updater/pkg"
13+
)
14+
15+
var cli struct {
16+
BundleFile string `type:"existingfile" short:"b" help:"Path to the Timoni bundle file to modify." required:"true"`
17+
Path string `arg:"" help:"A dot separated path to the value to override (must already exist)."`
18+
Value string `arg:"" help:"The value to override the value at the path with."`
19+
}
20+
21+
func main() {
22+
ctx := kong.Parse(&cli,
23+
kong.Name("updater"),
24+
kong.Description("A helper tool for modifying CUE files to override arbitrary values. Useful for updating Timoni bundles."))
25+
26+
cuectx := cuecontext.New()
27+
v, err := pkg.ReadFile(cuectx, cli.BundleFile)
28+
ctx.FatalIfErrorf(err)
29+
30+
if !v.LookupPath(cue.ParsePath(cli.Path)).Exists() {
31+
ctx.Fatalf("path %q does not exist", cli.Path)
32+
}
33+
34+
v, err = pkg.FillPathOverride(cuectx, v, cli.Path, cli.Value)
35+
ctx.FatalIfErrorf(err)
36+
37+
node := v.Syntax(cue.Final(), cue.Concrete(true))
38+
src, err := format.Node(node)
39+
ctx.FatalIfErrorf(err)
40+
41+
if err := os.WriteFile(cli.BundleFile, src, 0644); err != nil { //nolint:gosec
42+
ctx.Fatalf("failed to write file %q: %v", cli.BundleFile, err)
43+
}
44+
45+
os.Exit(0)
46+
}

tools/updater/go.mod

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
module github.com/input-output-hk/catalyst-ci/tools/updater
2+
3+
go 1.20
4+
5+
require (
6+
cuelang.org/go v0.7.0
7+
github.com/alecthomas/kong v0.8.1
8+
github.com/onsi/ginkgo/v2 v2.15.0
9+
github.com/onsi/gomega v1.31.0
10+
)
11+
12+
require (
13+
github.com/cockroachdb/apd/v3 v3.2.1 // indirect
14+
github.com/go-logr/logr v1.3.0 // indirect
15+
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
16+
github.com/google/go-cmp v0.6.0 // indirect
17+
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
18+
github.com/google/uuid v1.2.0 // indirect
19+
github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de // indirect
20+
golang.org/x/net v0.19.0 // indirect
21+
golang.org/x/sys v0.15.0 // indirect
22+
golang.org/x/text v0.14.0 // indirect
23+
golang.org/x/tools v0.16.1 // indirect
24+
gopkg.in/yaml.v3 v3.0.1 // indirect
25+
)

tools/updater/go.sum

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
cuelabs.dev/go/oci/ociregistry v0.0.0-20231103182354-93e78c079a13 h1:zkiIe8AxZ/kDjqQN+mDKc5BxoVJOqioSdqApjc+eB1I=
2+
cuelang.org/go v0.7.0 h1:gMztinxuKfJwMIxtboFsNc6s8AxwJGgsJV+3CuLffHI=
3+
cuelang.org/go v0.7.0/go.mod h1:ix+3dM/bSpdG9xg6qpCgnJnpeLtciZu+O/rDbywoMII=
4+
github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0=
5+
github.com/alecthomas/kong v0.8.1 h1:acZdn3m4lLRobeh3Zi2S2EpnXTd1mOL6U7xVml+vfkY=
6+
github.com/alecthomas/kong v0.8.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U=
7+
github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
8+
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
9+
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
10+
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
11+
github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg=
12+
github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc=
13+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
14+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
15+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
16+
github.com/emicklei/proto v1.10.0 h1:pDGyFRVV5RvV+nkBK9iy3q67FBy9Xa7vwrOTE+g5aGw=
17+
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
18+
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
19+
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
20+
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
21+
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
22+
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
23+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
24+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
25+
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
26+
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
27+
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
28+
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
29+
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
30+
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
31+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
32+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
33+
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
34+
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
35+
github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de h1:D5x39vF5KCwKQaw+OC9ZPiLVHXz3UFw2+psEX+gYcto=
36+
github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de/go.mod h1:kJun4WP5gFuHZgRjZUWWuH1DTxCtxbHDOIJsudS8jzY=
37+
github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY=
38+
github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM=
39+
github.com/onsi/gomega v1.31.0 h1:54UJxxj6cPInHS3a35wm6BK/F9nHYueZ1NVujHDrnXE=
40+
github.com/onsi/gomega v1.31.0/go.mod h1:DW9aCi7U6Yi40wNVAvT6kzFnEVEI5n3DloYBiKiT6zk=
41+
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
42+
github.com/opencontainers/image-spec v1.1.0-rc4 h1:oOxKUJWnFC4YGHCCMNql1x4YaDfYBTS5Y4x/Cgeo1E0=
43+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
44+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
45+
github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 h1:sadMIsgmHpEOGbUs6VtHBXRR1OHevnj7hLx9ZcdNGW4=
46+
github.com/rogpeppe/go-internal v1.11.1-0.20231026093722-fa6a31e0812c h1:fPpdjePK1atuOg28PXfNSqgwf9I/qD1Hlo39JFwKBXk=
47+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
48+
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
49+
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
50+
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
51+
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
52+
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
53+
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
54+
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
55+
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
56+
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
57+
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
58+
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
59+
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
60+
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
61+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
62+
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
63+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
64+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
65+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

tools/updater/pkg/cue.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package pkg
2+
3+
// cspell: words cuelang
4+
5+
import (
6+
"encoding/json"
7+
"fmt"
8+
"os"
9+
"strings"
10+
11+
"cuelang.org/go/cue"
12+
)
13+
14+
// FillPathOverride is like cue.Value.FillPath, but it allows you to override concrete values.
15+
// The default behavior of CUE is to support immutable values, so you can't normally override a concrete value.
16+
// This function first converts the cue.Value to JSON, then to a map, and then modifies the map.
17+
// Finally, it converts the map back to JSON and then to a cue.Value.
18+
func FillPathOverride(ctx *cue.Context, v cue.Value, path string, value interface{}) (cue.Value, error) {
19+
j, err := v.MarshalJSON()
20+
if err != nil {
21+
return cue.Value{}, fmt.Errorf("failed to marshal cue.Value to JSON: %w", err)
22+
}
23+
24+
var data map[string]interface{}
25+
if err := json.Unmarshal(j, &data); err != nil {
26+
return cue.Value{}, fmt.Errorf("failed to unmarshal JSON to map: %w", err)
27+
}
28+
29+
if err := setField(data, path, value); err != nil {
30+
return cue.Value{}, fmt.Errorf("failed to set field %q to value %v: %w", path, value, err)
31+
}
32+
33+
modifiedJSON, err := json.Marshal(data)
34+
if err != nil {
35+
return cue.Value{}, fmt.Errorf("failed to marshal map to JSON: %w", err)
36+
}
37+
38+
return ctx.CompileBytes(modifiedJSON), nil
39+
}
40+
41+
// ReadFile reads a CUE file and returns a cue.Value.
42+
func ReadFile(ctx *cue.Context, path string) (cue.Value, error) {
43+
contents, err := os.ReadFile(path)
44+
if err != nil {
45+
return cue.Value{}, fmt.Errorf("failed to read file %q: %w", path, err)
46+
}
47+
48+
v := ctx.CompileBytes(contents)
49+
if err := v.Err(); err != nil {
50+
return cue.Value{}, fmt.Errorf("failed to compile CUE file %q: %w", path, err)
51+
}
52+
53+
return v, nil
54+
}
55+
56+
// setField sets the value at the given path in a map, creating nested maps as necessary.
57+
func setField(m map[string]interface{}, path string, value interface{}) error {
58+
parts := strings.Split(path, ".")
59+
for i, part := range parts {
60+
if i == len(parts)-1 {
61+
m[part] = value
62+
} else {
63+
if _, ok := m[part]; !ok {
64+
m[part] = make(map[string]interface{})
65+
}
66+
var ok bool
67+
m, ok = m[part].(map[string]interface{})
68+
if !ok {
69+
return fmt.Errorf("invalid path: %s", path)
70+
}
71+
}
72+
}
73+
return nil
74+
}

tools/updater/pkg/cue_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package pkg_test
2+
3+
// cspell: words cuelang cuecontext
4+
5+
import (
6+
"cuelang.org/go/cue"
7+
"cuelang.org/go/cue/cuecontext"
8+
"github.com/input-output-hk/catalyst-ci/tools/updater/pkg"
9+
. "github.com/onsi/ginkgo/v2"
10+
. "github.com/onsi/gomega"
11+
)
12+
13+
var _ = Describe("Cue", func() {
14+
Describe("FillPathOverride", func() {
15+
var path, str string
16+
17+
When("given a nested CUE source", func() {
18+
When("the path exists", func() {
19+
BeforeEach(func() {
20+
path = "bundles.instances.module.values.image.tag"
21+
str = `
22+
bundle: {
23+
instances: {
24+
module: {
25+
values: {
26+
image: tag: "test"
27+
}
28+
}
29+
}
30+
}
31+
`
32+
})
33+
34+
It("should override the value at the given path", func() {
35+
ctx := cuecontext.New()
36+
v := ctx.CompileString(str)
37+
38+
v, err := pkg.FillPathOverride(ctx, v, path, "test1")
39+
Expect(err).ToNot(HaveOccurred())
40+
41+
result, err := v.LookupPath(cue.ParsePath(path)).String()
42+
Expect(err).ToNot(HaveOccurred())
43+
Expect(result).To(Equal("test1"))
44+
})
45+
})
46+
47+
When("the path does not include structs", func() {
48+
BeforeEach(func() {
49+
path = "bundles.instances.module.values.image.tag"
50+
str = `
51+
bundles: {
52+
instances: {
53+
module: "test"
54+
}
55+
}
56+
`
57+
})
58+
59+
It("should return an error", func() {
60+
ctx := cuecontext.New()
61+
v := ctx.CompileString(str)
62+
63+
_, err := pkg.FillPathOverride(ctx, v, path, "test1")
64+
Expect(err).To(HaveOccurred())
65+
})
66+
})
67+
})
68+
})
69+
})

0 commit comments

Comments
 (0)