Skip to content
Merged
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
19 changes: 18 additions & 1 deletion api/v1/tazuna_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ type TazunaSpec struct {
ContextMatches []string `json:"context_matches,omitempty"`
// ContextMatchModeはcontext_matchesの評価モードです("or" または "and"、デフォルトは "or")
ContextMatchMode ContextMatchMode `json:"context_match_mode,omitempty"`
Manifests []Manifest `json:"manifests"`
// Environments は環境ごとの設定を宣言するマップです。キーが環境名になります。
// `-e/--environment <name>` が渡されたとき、ルート直下の context_matches /
// context_match_mode ではなく、ここで宣言した環境固有の値が使われます。
// 未指定または `-e` が渡されない場合はルート直下の設定がそのまま使われます。
Environments map[string]EnvironmentSpec `json:"environments,omitempty"`
Manifests []Manifest `json:"manifests"`
// Testsはすべてのマニフェスト適用が終わったあとに実行されます
Tests []TestPluginSpec `json:"tests"`
// Providers は Secret provider の宣言リストです。未指定時は組み込みの "default-op"
Expand All @@ -49,6 +54,18 @@ type TazunaSpec struct {
Providers []ProviderConfig `json:"providers,omitempty"`
}

// EnvironmentSpec は 1 つの環境 (`environments.<name>`) の設定を定義します。
// `-e/--environment <name>` が渡されたとき、ルート直下の同名フィールドの代わりに
// ここで宣言した値が使われます。
type EnvironmentSpec struct {
// ContextMatches はこの環境で有効にする context_matches パターンのリストです。
// ルート直下の context_matches を完全に置き換えます (マージはしません)。
ContextMatches []string `json:"context_matches,omitempty"`
// ContextMatchMode はこの環境における context_matches の評価モードです。
// 空の場合はルート直下の context_match_mode を継承し、それも空なら "or" になります。
ContextMatchMode ContextMatchMode `json:"context_match_mode,omitempty"`
}

// IncludeFile はincludeするファイルを定義します
type IncludeFile struct {
// Path はincludeするファイルのパス(tazuna.yamlからの相対パス)
Expand Down
35 changes: 35 additions & 0 deletions api/v1/tazuna_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,41 @@ func TestTazunaSpec_RoundTrip(t *testing.T) {
}
}

func TestTazunaSpec_RoundTrip_Environments(t *testing.T) {
t.Parallel()
src := Tazuna{
Spec: TazunaSpec{
ContextMatches: []string{"^root-.*$"},
Environments: map[string]EnvironmentSpec{
"prod": {
ContextMatches: []string{"^prod-.*$"},
ContextMatchMode: ContextMatchModeAND,
},
"staging": {
ContextMatches: []string{"^staging-.*$"},
},
},
Manifests: []Manifest{{Name: "app", Type: ManifestTypeKustomize, Path: "./app"}},
},
}

got := roundTripTazuna(t, src)
if len(got.Spec.Environments) != 2 {
t.Fatalf("Environments len = %d, want 2", len(got.Spec.Environments))
}
prod := got.Spec.Environments["prod"]
if len(prod.ContextMatches) != 1 || prod.ContextMatches[0] != "^prod-.*$" {
t.Errorf("prod.ContextMatches = %v", prod.ContextMatches)
}
if prod.ContextMatchMode != ContextMatchModeAND {
t.Errorf("prod.ContextMatchMode = %q, want %q", prod.ContextMatchMode, ContextMatchModeAND)
}
staging := got.Spec.Environments["staging"]
if staging.ContextMatchMode != "" {
t.Errorf("staging.ContextMatchMode = %q, want empty", staging.ContextMatchMode)
}
}

func TestTazunaSpec_RoundTrip_OmitContextFields(t *testing.T) {
t.Parallel()
src := Tazuna{
Expand Down
18 changes: 15 additions & 3 deletions cmd/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,14 @@ untouched.
The target cluster is determined by the kubeconfig context.
When context_matches is configured, the current context name is validated.

With -e/--environment <name>, spec.environments.<name>.context_matches is used
instead of the root context_matches, and {{ .Environment }} in tazuna.yaml is
rendered to <name>.

Examples:
tazuna apply -f tazuna.yaml
tazuna apply -f tazuna.yaml --tags web,batch
tazuna apply -f tazuna.yaml -e production
tazuna apply -f tazuna.yaml --log-level debug
tazuna apply -f tazuna.yaml --sync
tazuna apply -f tazuna.yaml --sync --prune
Expand Down Expand Up @@ -102,16 +107,19 @@ Examples:
Atomic: atomic,
}

environment := cliutil.Environment(cmd)

r := runner.NewTazunaRunner(
logger,
k8sClient,
&op.CommandClient{},
runner.WithTags(tags),
runner.WithORASPullOptions(orasOpts),
runner.WithApplyOptions(applyOpts),
runner.WithEnvironment(environment),
)

tazuna, err := cliutil.LoadTazunaYAML(path)
tazuna, err := cliutil.LoadTazunaYAML(path, environment)
if err != nil {
return err
}
Expand All @@ -121,8 +129,12 @@ Examples:
return errors.Wrapf(err, "validation failed for tazuna.yaml at %s", path)
}

if len(tazuna.Spec.ContextMatches) > 0 {
if err := tazunacontext.ValidateCurrentContext(tazuna.Spec.ContextMatches, tazuna.Spec.ContextMatchMode); err != nil {
contextMatches, contextMatchMode, err := cliutil.ResolveContextMatches(tazuna.Spec, environment)
if err != nil {
return err
}
if len(contextMatches) > 0 {
if err := tazunacontext.ValidateCurrentContext(contextMatches, contextMatchMode); err != nil {
return err
}
}
Expand Down
5 changes: 3 additions & 2 deletions cmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ Examples:
if err != nil {
return err
}
r := runner.NewTazunaRunner(logger, k8sClient, &op.CommandClient{}, runner.WithTags(tags), runner.WithORASPullOptions(orasOpts))
environment := cliutil.Environment(cmd)
r := runner.NewTazunaRunner(logger, k8sClient, &op.CommandClient{}, runner.WithTags(tags), runner.WithORASPullOptions(orasOpts), runner.WithEnvironment(environment))

tazuna, err := cliutil.LoadTazunaYAML(path)
tazuna, err := cliutil.LoadTazunaYAML(path, environment)
if err != nil {
return err
}
Expand Down
10 changes: 8 additions & 2 deletions cmd/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Examples:
return errors.WithStack(err)
}

tazuna, err := cliutil.LoadTazunaYAML(path)
tazuna, err := cliutil.LoadTazunaYAML(path, cliutil.Environment(cmd))
if err != nil {
return err
}
Expand All @@ -63,11 +63,17 @@ Examples:
return errors.Wrapf(err, "validation failed for tazuna.yaml at %s", path)
}

// -e が渡されている場合、その環境が spec.environments に宣言されているかを
// この時点で検証しておくことで、apply/destroy 前に設定ミスを検知できる。
if _, _, err := cliutil.ResolveContextMatches(tazuna.Spec, cliutil.Environment(cmd)); err != nil {
return err
}

logger, err := cliutil.NewLogger(cmd)
if err != nil {
return err
}
r := runner.NewTazunaRunner(logger, nil, nil)
r := runner.NewTazunaRunner(logger, nil, nil, runner.WithEnvironment(cliutil.Environment(cmd)))

if fix {
if err := r.CheckAndFix(ctx, tazuna, absPath); err != nil {
Expand Down
13 changes: 9 additions & 4 deletions cmd/destroy.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,10 @@ Examples:
if err != nil {
return err
}
r := runner.NewTazunaRunner(logger, k8sClient, &op.CommandClient{}, runner.WithTags(tags), runner.WithORASPullOptions(orasOpts))
environment := cliutil.Environment(cmd)
r := runner.NewTazunaRunner(logger, k8sClient, &op.CommandClient{}, runner.WithTags(tags), runner.WithORASPullOptions(orasOpts), runner.WithEnvironment(environment))

tazuna, err := cliutil.LoadTazunaYAML(path)
tazuna, err := cliutil.LoadTazunaYAML(path, environment)
if err != nil {
return err
}
Expand All @@ -71,8 +72,12 @@ Examples:
return errors.Wrapf(err, "validation failed for tazuna.yaml at %s", path)
}

if len(tazuna.Spec.ContextMatches) > 0 {
if err := tazunacontext.ValidateCurrentContext(tazuna.Spec.ContextMatches, tazuna.Spec.ContextMatchMode); err != nil {
contextMatches, contextMatchMode, err := cliutil.ResolveContextMatches(tazuna.Spec, environment)
if err != nil {
return err
}
if len(contextMatches) > 0 {
if err := tazunacontext.ValidateCurrentContext(contextMatches, contextMatchMode); err != nil {
return err
}
}
Expand Down
1 change: 1 addition & 0 deletions cmd/flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func TestRootPersistentFlags(t *testing.T) {
for _, spec := range []flagSpec{
{name: "file-path", defaultVal: "tazuna.yaml"},
{name: "log-level", defaultVal: "info"},
{name: "environment", defaultVal: ""},
} {
if f := rootCmd.PersistentFlags().Lookup(spec.name); f == nil {
t.Errorf("root persistent flag %q missing", spec.name)
Expand Down
51 changes: 48 additions & 3 deletions cmd/internal/cliutil/cliutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/Masterminds/semver/v3"
"github.com/cockroachdb/errors"
v1 "github.com/pepabo/tazuna/api/v1"
"github.com/pepabo/tazuna/pkg/tmpl"
"github.com/spf13/cobra"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand Down Expand Up @@ -56,14 +57,22 @@ func NewLogger(cmd *cobra.Command) (*slog.Logger, error) {
return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: ParseLogLevel(logLevelS)})), nil
}

// LoadTazunaYAML reads path and decodes it as a v1.Tazuna document.
func LoadTazunaYAML(path string) (*v1.Tazuna, error) {
// LoadTazunaYAML reads path, renders it as a Go template, and decodes it as a
// v1.Tazuna document.
//
// environment は -e/--environment フラグの値で、テンプレート内の {{ .Environment }}
// に注入されます。フラグが渡されていない場合は空文字列を渡してください。
func LoadTazunaYAML(path, environment string) (*v1.Tazuna, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, errors.WithStack(err)
}
rendered, err := tmpl.Render(path, data, tmpl.Data{Environment: environment})
if err != nil {
return nil, errors.WithStack(err)
}
tazuna := v1.Tazuna{}
if err := yaml.Unmarshal(data, &tazuna); err != nil {
if err := yaml.Unmarshal(rendered, &tazuna); err != nil {
return nil, errors.WithStack(err)
}
if err := CheckMinimumSupportedVersion(&tazuna, currentVersion); err != nil {
Expand All @@ -72,6 +81,42 @@ func LoadTazunaYAML(path string) (*v1.Tazuna, error) {
return &tazuna, nil
}

// ResolveContextMatches は environment に応じて有効な context_matches パターンと
// 評価モードを返します。
//
// - environment が空文字列の場合、ルート直下の spec.context_matches /
// spec.context_match_mode をそのまま返します。
// - environment が指定された場合、spec.environments[environment] の値を使います。
// 該当する環境が宣言されていなければエラーになります。環境の context_match_mode が
// 空なら、ルート直下の context_match_mode を継承します。
func ResolveContextMatches(spec v1.TazunaSpec, environment string) ([]string, v1.ContextMatchMode, error) {
if environment == "" {
return spec.ContextMatches, spec.ContextMatchMode, nil
}

env, ok := spec.Environments[environment]
if !ok {
return nil, "", errors.Errorf(
"environment %q is not declared under spec.environments", environment)
}

mode := env.ContextMatchMode
if mode == "" {
mode = spec.ContextMatchMode
}
return env.ContextMatches, mode, nil
}

// Environment は永続フラグ -e/--environment の値を読み取ります。フラグが未登録の
// サブコマンドでは空文字列を返します。
func Environment(cmd *cobra.Command) string {
env, err := cmd.Flags().GetString("environment")
if err != nil {
return ""
}
return env
}

// CheckMinimumSupportedVersion は spec.minimumSupportedTazunaVersion と実行中の
// tazuna バージョン current を比較します。current が制約を満たさない場合はエラーを
// 返します。
Expand Down
Loading