Skip to content

Commit 9365cfe

Browse files
authored
Common loader improvements (#127)
* Improve loader. Better support env variables. Force array for a command, plain string is never used and adds complexity. * Rework loader. Execute templates only on string values instead of a whole file. Add support for multiline values in action.yaml. Add new template functions: default, config Deprecate removeLine and others because we can't delete a line with the new processing. It conflicts with multiline support. Refactor out Services for better testing. * Refactor services and the way they are created. * Add YamlQuery template func.
1 parent ea2d6c8 commit 9365cfe

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1561
-857
lines changed

.github/workflows/test-suite.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ jobs:
6868
os: ubuntu-24.04-arm
6969
- name: 🍎 MacOS (amd64)
7070
os: macos-13
71+
continue-on-error: true
7172
- name: 🍎 MacOS (arm64)
7273
os: macos-latest
7374
needs-sidecar: true
@@ -76,6 +77,7 @@ jobs:
7677
- name: 🖥️ Windows (arm64)
7778
os: windows-11-arm
7879
needs-sidecar: true
80+
continue-on-error: true
7981
runs-on: ${{ matrix.os }}
8082
needs: [ client-ssh-key ]
8183
continue-on-error: ${{ matrix.continue-on-error || false }}
@@ -139,7 +141,6 @@ jobs:
139141
name: test-log-${{ matrix.os }}
140142
path: .gotmp/gotest.log
141143
retention-days: 3
142-
if-no-files-found: error
143144

144145
lint:
145146
name: 🧹 Lint & Code Quality

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ LOCAL_BIN:=$(CURDIR)/bin
2323

2424
# Linter config.
2525
GOLANGCI_BIN:=$(LOCAL_BIN)/golangci-lint
26-
GOLANGCI_TAG:=2.3.0
26+
GOLANGCI_TAG:=2.5.0
2727

2828
GOTESTFMT_BIN:=$(GOBIN)/gotestfmt
2929

app.go

Lines changed: 23 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,10 @@ package launchr
22

33
import (
44
"errors"
5-
"fmt"
65
"io"
76
"os"
8-
"reflect"
97

108
"github.com/launchrctl/launchr/internal/launchr"
11-
"github.com/launchrctl/launchr/pkg/action"
129
_ "github.com/launchrctl/launchr/plugins" // include default plugins
1310
)
1411

@@ -20,11 +17,11 @@ type appImpl struct {
2017
// FS related.
2118
mFS []ManagedFS
2219
workDir string
23-
cfgDir string
2420

2521
// Services.
2622
streams Streams
27-
services map[ServiceInfo]Service
23+
mask *SensitiveMask
24+
services *ServiceManager
2825
pluginMngr PluginManager
2926
}
3027

@@ -40,46 +37,20 @@ func (app *appImpl) SetStreams(s Streams) { app.streams = s }
4037
func (app *appImpl) RegisterFS(fs ManagedFS) { app.mFS = append(app.mFS, fs) }
4138
func (app *appImpl) GetRegisteredFS() []ManagedFS { return app.mFS }
4239

43-
func (app *appImpl) SensitiveWriter(w io.Writer) io.Writer {
44-
return NewMaskingWriter(w, app.SensitiveMask())
45-
}
46-
func (app *appImpl) SensitiveMask() *SensitiveMask { return launchr.GlobalSensitiveMask() }
40+
func (app *appImpl) SensitiveWriter(w io.Writer) io.Writer { return app.SensitiveMask().MaskWriter(w) }
41+
func (app *appImpl) SensitiveMask() *SensitiveMask { return app.mask }
4742

4843
func (app *appImpl) RootCmd() *Command { return app.cmd }
4944
func (app *appImpl) CmdEarlyParsed() launchr.CmdEarlyParsed { return app.earlyCmd }
5045

46+
func (app *appImpl) Services() *ServiceManager { return app.services }
47+
5148
func (app *appImpl) AddService(s Service) {
52-
info := s.ServiceInfo()
53-
launchr.InitServiceInfo(&info, s)
54-
if _, ok := app.services[info]; ok {
55-
panic(fmt.Errorf("service %s already exists, review your code", info))
56-
}
57-
app.services[info] = s
49+
app.services.Add(s)
5850
}
5951

6052
func (app *appImpl) GetService(v any) {
61-
// Check v is a pointer and implements [Service] to set a value later.
62-
t := reflect.TypeOf(v)
63-
isPtr := t != nil && t.Kind() == reflect.Pointer
64-
var stype reflect.Type
65-
if isPtr {
66-
stype = t.Elem()
67-
}
68-
69-
// v must be [Service] but can't equal it because all elements implement it
70-
// and the first value will always be returned.
71-
intService := reflect.TypeOf((*Service)(nil)).Elem()
72-
if !isPtr || !stype.Implements(intService) || stype == intService {
73-
panic(fmt.Errorf("argument must be a pointer to a type (interface) implementing Service, %q given", t))
74-
}
75-
for _, srv := range app.services {
76-
st := reflect.TypeOf(srv)
77-
if st.AssignableTo(stype) {
78-
reflect.ValueOf(v).Elem().Set(reflect.ValueOf(srv))
79-
return
80-
}
81-
}
82-
panic(fmt.Sprintf("service %q does not exist", stype))
53+
app.services.Get(v)
8354
}
8455

8556
// init initializes application and plugins.
@@ -108,34 +79,25 @@ func (app *appImpl) init() error {
10879
}
10980
app.cmd.SetVersionTemplate(`{{ appVersionFull }}`)
11081
app.earlyCmd = launchr.EarlyPeekCommand()
111-
// Set io streams.
112-
app.SetStreams(MaskedStdStreams(app.SensitiveMask()))
113-
app.cmd.SetIn(app.streams.In().Reader())
114-
app.cmd.SetOut(app.streams.Out())
115-
app.cmd.SetErr(app.streams.Err())
82+
app.mask = launchr.NewSensitiveMask("****")
11683

117-
// Set working dir and config dir.
118-
app.cfgDir = "." + name
119-
app.workDir = launchr.MustAbs(".")
120-
actionsPath := launchr.MustAbs(EnvVarActionsPath.Get())
121-
// Initialize managed FS for action discovery.
84+
// Initialize managed FS for action discovery and set working dir
85+
app.workDir = MustAbs(".")
12286
app.mFS = make([]ManagedFS, 0, 4)
123-
app.RegisterFS(action.NewDiscoveryFS(os.DirFS(actionsPath), app.GetWD()))
12487

12588
// Prepare dependencies.
126-
app.services = make(map[ServiceInfo]Service)
89+
app.services = launchr.NewServiceManager()
12790
app.pluginMngr = launchr.NewPluginManagerWithRegistered()
128-
// @todo consider home dir for global config.
129-
config := launchr.ConfigFromFS(os.DirFS(app.cfgDir))
130-
actionMngr := action.NewManager(
131-
action.WithDefaultRuntime(config),
132-
action.WithContainerRuntimeConfig(config, name+"_"),
133-
)
134-
135-
// Register services for other modules.
136-
app.AddService(actionMngr)
137-
app.AddService(app.pluginMngr)
138-
app.AddService(config)
91+
92+
// Register svcMngr for other modules.
93+
app.services.Add(app.mask)
94+
app.services.Add(app.pluginMngr)
95+
96+
// Set io streams.
97+
app.SetStreams(MaskedStdStreams(app.mask))
98+
app.cmd.SetIn(app.streams.In().Reader())
99+
app.cmd.SetOut(app.streams.Out())
100+
app.cmd.SetErr(app.streams.Err())
139101

140102
Log().Debug("initialising application")
141103

@@ -148,7 +110,7 @@ func (app *appImpl) init() error {
148110
return err
149111
}
150112
}
151-
Log().Debug("init success", "wd", app.workDir, "actions_dir", actionsPath)
113+
Log().Debug("init success", "wd", app.workDir)
152114

153115
return nil
154116
}

docs/actions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ runtime:
6868
command:
6969
- python3
7070
- {{ .myArg1 }} {{ .myArg2 }}
71-
- {{ .optStr }}
71+
- '{{ .optStr }}'
7272
- ${ENV_VAR}
7373
```
7474

docs/actions.schema.md

Lines changed: 24 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ The action provides basic templating for all file based on arguments, options an
189189
For templating, the standard Go templating engine is used.
190190
Refer to [the library documentation](https://pkg.go.dev/text/template) for usage examples.
191191

192+
Since `action.yaml` must be valid YAML, wrap template strings in quotes to ensure proper parsing.
193+
192194
Arguments and Options are available by their machine names - `{{ .myArg1 }}`, `{{ .optStr }}`, `{{ .optArr }}`, etc.
193195

194196
### Predefined variables:
@@ -227,85 +229,61 @@ runtime:
227229
image: {{ .optStr }}:latest
228230
command:
229231
- {{ .myArg1 }} {{ .MyArg2 }}
230-
- {{ .optBool }}
232+
- '{{ .optBool }}'
231233
```
232234
233235
### Available Command Template Functions
234236
235-
### `removeLine`
236-
**Description:** A special template directive that removes the entire line from the final output.
237-
238-
**Usage:**
239-
240-
``` yaml
241-
- "{{ if condition }}value{{ else }}{{ removeLine }}{{ end }}"
242-
```
243-
244237
### `isNil`
245238

246239
**Description:** Checks if a value is nil.
247240

248241
**Usage:**
249242

250-
```yaml
251-
- "{{ if not (isNil .param_name) }}--param={{ .param_name }}{{ else }}{{ removeLine }}{{ end }}"
243+
```gotemplate
244+
{{ if not (isNil .param_name) }}--param={{ .param_name }}{{ end }}
252245
```
253246

254247
### `isSet`
255248

256249
**Description:** Checks if a value has been set (opposite of `isNil`).
257250

258-
```yaml
259-
- "{{ if isSet .param_name }}--param={{ .param_name }}{{else}}{{ removeLine }}{{ end }}"
251+
```gotemplate
252+
{{ if isSet .param_name }}--param={{ .param_name }}{{ end }}
260253
```
261254

262255
### `isChanged`
263256

264-
**Description:** Checks if an option or argument value has been changed (dirty).
257+
**Description:** Checks if an option or argument value has been changed (input by user).
265258

266259
**Usage:**
267260

268-
```yaml
269-
- '{{ if isChanged "param_name"}}--param={{.param_name}}{{else}}{{ removeLine }}{{ end }}'
261+
```gotemplate
262+
{{ if isChanged "param_name"}}--param={{.param_name}}{{ end }}
270263
```
271264

272-
### `removeLineIfNil`
273-
**Description:** Removes the entire command line if the value is nil.
265+
### `default`
274266

275-
**Usage:**
276-
277-
```yaml
278-
- "{{ removeLineIfNil .param_name }}"
279-
```
280-
281-
### `removeLineIfSet`
282-
**Description:** Removes the entire command line if the value is set (has no nil value).
267+
**Description:** Returns a default value when the first parameter is `nil` or empty.
268+
Emptiness is determined by its zero value - empty string `""`, integer `0`, structs with all zero-value fields, etc.
269+
Or type implements `interface { IsEmpty() bool }`.
283270

284271
**Usage:**
285-
286-
```yaml
287-
- "{{ removeLineIfSet .param_name }}"
272+
```gotemplate
273+
{{ .nil_value | default "foo" }}
274+
{{ default .nil_value "bar" }}
288275
```
289276

290-
### `removeLineIfChanged`
277+
### `config.Get`
291278

292-
**Description:** Removes the command line entry if the option/argument value has changed.
279+
**Description:** Returns a [config](config.md) value by a path.
293280

294281
**Usage:**
295-
296-
``` yaml
297-
- '{{ removeLineIfChanged "param_name" }}'
298-
```
299-
300-
### `removeLineIfNotChanged`
301-
302-
**Description:** Removes the command line entry if the option/argument value has not changed by the user.
303-
Opposite of `removeLineIfChanged`
304-
305-
**Usage:**
306-
307-
``` yaml
308-
- '{{ removeLineIfNotChanged "param_name" }}'
282+
```gotemplate
283+
{{ config "foo.bar" }} # retrieves value of any type
284+
{{ index (config "foo.array-elem") 1 }} # retrieves specific array element
285+
{{ config "foo.null-elem" | default "foo" }} # uses default if value is nil
286+
{{ config "foo.missing-elem" | default "bar" }} # uses default if key doesn't exist
309287
```
310288

311289

docs/development/plugin.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ Add the processor to the Action Manager:
169169

170170
```go
171171
var am action.Manager
172-
app.GetService(&am)
172+
app.Services().Get(&am)
173173

174174
procReplace := GenericValueProcessor[procTestReplaceOptions]{
175175
Types: []jsonschema.Type{jsonschema.String},

docs/development/service.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import (
2525
// Get a service from the App.
2626
func (p *Plugin) OnAppInit(app launchr.App) error {
2727
var cfg launchr.Config
28-
app.GetService(&cfg) // Pass a pointer to init the value.
28+
app.Services().Get(&cfg) // Pass a pointer to init the value.
2929
return nil
3030
}
3131
```
@@ -41,7 +41,7 @@ import (
4141
)
4242

4343
// Define a service and implement service interface.
44-
// It is important to have a unique interface, the service is identified by it in launchr.GetService().
44+
// It is important to have a unique interface, the service is identified by it in [launchr.ServiceManager].Get().
4545
type ExampleService interface {
4646
launchr.Service // Inherit launchr.Service
4747
// Provide other methods if needed.
@@ -58,7 +58,7 @@ func (ex *exampleSrvImpl) ServiceInfo() launchr.ServiceInfo {
5858
// Register a service inside launchr.
5959
func (p *Plugin) OnAppInit(app launchr.App) error {
6060
srv := &exampleSrvImpl{}
61-
app.AddService(srv)
61+
app.Services().Add(srv)
6262
return nil
6363
}
6464
```

docs/development/test.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ concurrency:
198198
jobs:
199199
tests:
200200
name: 🛡️ Multi-Platform Testing Suite
201-
uses: launchr/launchr/.github/workflows/test-suite.yaml@main
201+
uses: launchrctl/launchr/.github/workflows/test-suite.yaml@main
202202
```
203203
204204
### Benefits of Reusable Workflows

example/actions/arguments/action.yaml

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,7 @@ runtime:
3232
- /action/main.sh
3333
- "{{ .arg1 }}"
3434
- "{{ .arg2 }}"
35-
- "{{ .firstoption|removeLineIfNil }}"
36-
- "{{ if not (isNil .secondoption) }}--secondoption={{ .secondoption }}{{ else }}{{ removeLine }}{{ end }}"
35+
- "{{ if not (isNil .secondoption) }}--secondoption={{ .secondoption }}{{else}}Second option is nil{{ end }}"
3736
- "{{ if isSet .thirdoption }}--thirdoption={{ .thirdoption }}{{else}}Third option is not set{{ end }}"
38-
- "{{ removeLineIfSet .thirdoption }}"
39-
- '{{ if not (isChanged "thirdoption")}}Third Option is not Changed{{else}}{{ removeLine }}{{ end }}'
40-
- '{{ removeLineIfChanged "thirdoption" }}'
41-
- '{{ if isChanged "thirdoption"}}Third Option is Changed{{else}}{{ removeLine }}{{ end }}'
42-
- '{{ removeLineIfNotChanged "thirdoption" }}'
37+
- '{{ if not (isChanged "thirdoption")}}Third Option is not Changed{{ end }}'
38+
- '{{ if isChanged "thirdoption"}}Third Option is Changed{{ end }}'

example/actions/platform/actions/build/action.yaml

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,16 @@ action:
5858

5959
runtime:
6060
type: container
61-
# image: python:3.7-slim
6261
image: ubuntu
63-
# command: python3 {{ .opt4 }}
64-
command: ["sh", "-c", "for i in $(seq 60); do if [ $((i % 2)) -eq 0 ]; then echo \"stdout: $$i\"; else echo \"stderr: $$i\" >&2; fi; sleep 1; done"]
65-
# command: /bin/bash
62+
command:
63+
- "sh"
64+
- "-c"
65+
- |
66+
for i in $(seq 60); do
67+
if [ $((i % 2)) -eq 0 ]; then
68+
echo "stdout: $$i";
69+
else
70+
echo "stderr: $$i" >&2;
71+
fi;
72+
sleep 1;
73+
done

0 commit comments

Comments
 (0)