Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,37 @@ Priority: `--no-color` flag > `NO_COLOR` > `ECSCHEDULE_COLOR` > default (colored

**Note**: The `--no-color` flag is a subcommand-level flag and only affects unified diff format. The default pretty format always displays with colors.

### Jsonnet external variables

When the configuration file is a Jsonnet (`*.jsonnet`) file, you can pass values
to `std.extVar` from the command line using top-level `-ext-str` / `-ext-code`
flags, mirroring the upstream `jsonnet` CLI:

```console
% ecschedule -conf ecschedule.jsonnet \
-ext-str CLUSTER=api -ext-str REGION=us-east-1 \
-ext-code 'TASK_COUNT=2' \
apply -all
```

`-ext-str key=value` binds a string. `-ext-code key=value` binds a Jsonnet
expression (numbers, arrays, objects, etc.). Either form may also be passed as
just `-ext-str KEY` / `-ext-code KEY`, in which case the value is read from the
environment variable of the same name. Both flags may be repeated.

```jsonnet
{
region: std.extVar('REGION'),
cluster: std.extVar('CLUSTER'),
rules: [
{
// ...
taskCount: std.extVar('TASK_COUNT'), // -ext-code so it stays a number
},
],
}
```

## Functions

You can use following functions in the configuration file.
Expand Down
2 changes: 1 addition & 1 deletion cmd_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ var cmdApply = &runnerImpl{
return err
}
defer f.Close()
c, err = LoadConfig(ctx, f, a.AccountID, *conf)
c, err = LoadConfig(ctx, f, a.AccountID, *conf, a.loadConfigOptions()...)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd_diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ var cmdDiff = &runnerImpl{
return err
}
defer f.Close()
c, err = LoadConfig(ctx, f, a.AccountID, *conf)
c, err = LoadConfig(ctx, f, a.AccountID, *conf, a.loadConfigOptions()...)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd_dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ var cmdDump = &runnerImpl{
return err
}
defer f.Close()
c, err = LoadConfig(ctx, f, a.AccountID, *conf)
c, err = LoadConfig(ctx, f, a.AccountID, *conf, a.loadConfigOptions()...)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ var cmdRun = &runnerImpl{
return err
}
defer f.Close()
c, err = LoadConfig(ctx, f, a.AccountID, *conf)
c, err = LoadConfig(ctx, f, a.AccountID, *conf, a.loadConfigOptions()...)
if err != nil {
return err
}
Expand Down
48 changes: 45 additions & 3 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,46 @@ func validateCronExpression(exp string) error {
return nil
}

type loadConfigOptions struct {
extStr map[string]string
extCode map[string]string
}

// LoadConfigOption configures LoadConfig
type LoadConfigOption func(*loadConfigOptions)

// WithExtStr binds Jsonnet std.extVar string variables
func WithExtStr(vars map[string]string) LoadConfigOption {
return func(o *loadConfigOptions) {
if o.extStr == nil {
o.extStr = map[string]string{}
}
for k, v := range vars {
o.extStr[k] = v
}
}
}

// WithExtCode binds Jsonnet std.extVar code variables
func WithExtCode(vars map[string]string) LoadConfigOption {
return func(o *loadConfigOptions) {
if o.extCode == nil {
o.extCode = map[string]string{}
}
for k, v := range vars {
o.extCode[k] = v
}
}
}

// LoadConfig loads config
func LoadConfig(ctx context.Context, r io.Reader, accountID string, confPath string) (*Config, error) {
func LoadConfig(ctx context.Context, r io.Reader, accountID string, confPath string, opts ...LoadConfigOption) (*Config, error) {
var o loadConfigOptions
for _, opt := range opts {
opt(&o)
}
c := Config{}
bs, ext, err := readConfigFile(r, confPath)
bs, ext, err := readConfigFile(r, confPath, &o)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -152,10 +188,16 @@ func unmarshalConfig(bs []byte, c *Config, ext string) error {
return yaml.Unmarshal(bs, c)
}

func readConfigFile(r io.Reader, confPath string) ([]byte, string, error) {
func readConfigFile(r io.Reader, confPath string, o *loadConfigOptions) ([]byte, string, error) {
ext := filepath.Ext(confPath)
if ext == jsonnetExt {
vm := newJsonnetVM()
for k, v := range o.extStr {
vm.ExtVar(k, v)
}
for k, v := range o.extCode {
vm.ExtCode(k, v)
}
bs, err := vm.EvaluateFile(confPath)
if err != nil {
return nil, ext, fmt.Errorf("failed to evaluate jsonnet file: %w", err)
Expand Down
51 changes: 50 additions & 1 deletion config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ecschedule
import (
"context"
"os"
"path/filepath"
"reflect"
"testing"
"text/template"
Expand Down Expand Up @@ -88,7 +89,19 @@ func TestLoadConfig(t *testing.T) {
t.Fatal(err)
}
defer f.Close()
c, err := LoadConfig(context.Background(), f, "334", path)
var opts []LoadConfigOption
if filepath.Ext(path) == jsonnetExt {
opts = append(opts,
WithExtStr(map[string]string{
"REGION": "us-east-1",
"CLUSTER": "api",
}),
WithExtCode(map[string]string{
"BASE": "1",
}),
)
}
c, err := LoadConfig(context.Background(), f, "334", path, opts...)
if err != nil {
t.Errorf("error should be nil, but: %s", err)
}
Expand Down Expand Up @@ -205,6 +218,42 @@ func TestLoadConfig_tfstate_multi(t *testing.T) {
}
}

func TestLoadConfig_tfstate_multi_jsonnet(t *testing.T) {
path := "testdata/sample5.jsonnet"
f, err := os.Open(path)
if err != nil {
t.Fatal(err)
}
defer f.Close()

c, err := LoadConfig(context.Background(), f, "338", path)
if err != nil {
t.Fatalf("error should be nil, but: %s", err)
}

if !reflect.DeepEqual(c.Plugins, []*Plugin{
{Name: "tfstate", Config: map[string]interface{}{"path": "testdata/terraform.tfstate"}, FuncPrefix: "first_"},
{Name: "tfstate", Config: map[string]interface{}{"path": "testdata/terraform.tfstate"}, FuncPrefix: "second_"},
}) {
t.Errorf("unexpected output: %#v", c.Plugins)
}
}

func TestLoadConfig_jsonnetExtVar_missing(t *testing.T) {
// std.extVar references in sample.jsonnet must be supplied; otherwise
// jsonnet should error rather than silently substituting an empty value.
path := "testdata/sample.jsonnet"
f, err := os.Open(path)
if err != nil {
t.Fatal(err)
}
defer f.Close()

if _, err := LoadConfig(context.Background(), f, "334", path); err == nil {
t.Error("expected error when std.extVar bindings are missing, got nil")
}
}

func TestCronValidate(t *testing.T) {
c := &Config{
Rules: []*Rule{
Expand Down
13 changes: 13 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ type app struct {
Config *Config
AccountID string
AwsConf aws.Config
ExtStr map[string]string
ExtCode map[string]string
}

func (a *app) loadConfigOptions() []LoadConfigOption {
var opts []LoadConfigOption
if len(a.ExtStr) > 0 {
opts = append(opts, WithExtStr(a.ExtStr))
}
if len(a.ExtCode) > 0 {
opts = append(opts, WithExtCode(a.ExtCode))
}
return opts
}

func setApp(ctx context.Context, a *app) context.Context {
Expand Down
50 changes: 47 additions & 3 deletions ecsched.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,50 @@ import (
"io"
"log"
"os"
"strings"

"github.com/aws/aws-sdk-go-v2/config"
)

const cmdName = "ecschedule"

// extVarFlag accumulates repeated -ext-str/-ext-code key=value (or bare key, read from env) pairs
type extVarFlag struct {
pairs map[string]string
}

func newExtVarFlag() *extVarFlag {
return &extVarFlag{pairs: map[string]string{}}
}

func (e *extVarFlag) String() string {
parts := make([]string, 0, len(e.pairs))
for k, v := range e.pairs {
parts = append(parts, k+"="+v)
}
return strings.Join(parts, ",")
}

func (e *extVarFlag) Set(s string) error {
if i := strings.IndexByte(s, '='); i >= 0 {
key := s[:i]
if key == "" {
return fmt.Errorf("empty key in %q", s)
}
e.pairs[key] = s[i+1:]
return nil
}
if s == "" {
return fmt.Errorf("empty key")
}
v, ok := os.LookupEnv(s)
if !ok {
return fmt.Errorf("environment variable %q is not defined", s)
}
e.pairs[s] = v
return nil
}

// Run the ecschedule
func Run(ctx context.Context, argv []string, outStream, errStream io.Writer) error {
log.SetOutput(errStream)
Expand All @@ -28,9 +66,13 @@ func Run(ctx context.Context, argv []string, outStream, errStream io.Writer) err
formatCommands(fs.Output())
}
var (
conf = fs.String("conf", "", "configuration")
ver = fs.Bool("version", false, "display version")
conf = fs.String("conf", "", "configuration")
ver = fs.Bool("version", false, "display version")
extStr = newExtVarFlag()
extCode = newExtVarFlag()
)
fs.Var(extStr, "ext-str", "jsonnet std.extVar string binding (key=value, or just key to read from env)")
fs.Var(extCode, "ext-code", "jsonnet std.extVar code binding (key=value, or just key to read from env)")
if err := fs.Parse(argv); err != nil {
return err
}
Expand All @@ -48,6 +90,8 @@ func Run(ctx context.Context, argv []string, outStream, errStream io.Writer) err
a := &app{
AccountID: accountID,
AwsConf: awsConf,
ExtStr: extStr.pairs,
ExtCode: extCode.pairs,
}
ctx = setApp(ctx, a)
if *conf != "" {
Expand All @@ -56,7 +100,7 @@ func Run(ctx context.Context, argv []string, outStream, errStream io.Writer) err
return err
}
defer f.Close()
c, err := LoadConfig(ctx, f, a.AccountID, *conf)
c, err := LoadConfig(ctx, f, a.AccountID, *conf, a.loadConfigOptions()...)
if err != nil {
return err
}
Expand Down
6 changes: 3 additions & 3 deletions plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import (

// Plugin the plugin
type Plugin struct {
Name string `yaml:"name"`
Config map[string]interface{} `yaml:"config"`
FuncPrefix string `yaml:"func_prefix,omitempty"`
Name string `yaml:"name" json:"name"`
Config map[string]interface{} `yaml:"config" json:"config"`
FuncPrefix string `yaml:"func_prefix,omitempty" json:"func_prefix,omitempty"`
}

func (p Plugin) setup(ctx context.Context, c *Config) error {
Expand Down
6 changes: 3 additions & 3 deletions testdata/sample.jsonnet
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
local envs = import 'envs.libsonnet';

{
"region": "us-east-1",
"cluster": "api",
"region": std.extVar('REGION'),
"cluster": std.extVar('CLUSTER'),
"role": "ecsEventsRole",
"rules": [
{
Expand All @@ -16,7 +16,7 @@ local envs = import 'envs.libsonnet';
"capacityProviderStrategy": [
{
"capacityProvider": "FARGATE",
"base": 1,
"base": std.extVar('BASE'),
"weight": 1
}
],
Expand Down
Loading