Skip to content
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,26 @@
limit is used; `-t json|yaml` prints the platform's summary. `license report`
downloads the license usage report, a zip archive, under the platform's name without
overwriting a file, or to `-o`. Both need an admin access token.
- `export --tenant -d dir` writes the tenant's configuration, to keep in Git: experiment
templates, environments, teams, property definitions, hubs, webhook, Slack and preflight
integrations and custom service profiles. `diff -d dir` and `apply -d dir [--dry-run]`
take such a directory as they take a team's, and apply property definitions,
environments, teams, hubs, templates, integrations and profiles in that order, before
services, experiments and schedules. Hubs are synchronized as they are applied, so
that the service profiles find the templates they name. Hubs the platform connects
itself, templates imported from a hub and Steadybit's service profiles are left out,
as every platform has them. Teams are kept without their id, which differs between
platforms, and matched by their key; `team apply` no longer sends the id, which the
platform ignores.
- Credentials of integrations are masked in exported files, and never printed by `diff`.
A masked value matches whatever the platform holds, so that an exported tenant shows no
drift and applies again: integrations that match are not applied, as the platform keeps
no secret it is not sent, and `integration ... apply` sends the stored header values and
Slack URLs in place of their masks, and leaves out a file with a masked secret that
matches the platform.
- Experiment templates, environments, teams, property definitions, hubs and integrations
have a `diff`, and their `apply` a `--dry-run`. The actions a team is given when sent
none, and the target attributes a webhook reports when sent none, are not differences.

## v6.0.1

Expand Down
29 changes: 27 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,9 +280,34 @@ steadybit apply -d ./chaos --dry-run # what an apply would create or update
steadybit apply -d ./chaos # profiles, services, experiments, then schedules
```

Keep the tenant's configuration in Git the same way: experiment templates, environments,
teams, property definitions, hubs, integrations and custom service profiles, one directory
each (`templates/`, `environments/`, `teams/`, `property-definitions/`, `hubs/`,
`integrations/<kind>/`, `service-profiles/`):

```bash
steadybit export --tenant -d ./platform # needs an admin access token
steadybit diff -d ./platform
steadybit apply -d ./platform --dry-run
steadybit apply -d ./platform # definitions, environments, teams, hubs, templates, integrations, profiles
```

What the platform provides is left out: the hubs it connects, the templates imported from
a hub (`template import` brings them back) and Steadybit's service profiles. Hubs are
synchronized as they are applied, so that the service profiles find their templates.

Credentials of integrations (secrets, header values, Slack webhook URLs) are written as
`'********'`. A mask stands for what the platform holds: `diff` does not report it, and
`apply` leaves out the integrations that match the platform and sends the stored header
values and URLs in place of their masks. The platform never reads a secret back, so to
change an integration that has one, put the secret in, e.g. from a CI secret, before
applying. As the platform cannot say whether that is the secret it holds, such a file is
always a difference. `diff` never prints credentials.

Each kind also has its own `diff`, and its `apply` a `--dry-run`, e.g.
`steadybit experiment diff -f ./experiments -R`. Fields the platform fills in with defaults
are not reported as differences.
`steadybit experiment diff -f ./experiments -R` or
`steadybit integration webhook diff -f ./platform/integrations/webhook -R`. Fields the
platform fills in with defaults are not reported as differences.

## In CI

Expand Down
4 changes: 3 additions & 1 deletion internal/cli/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/spf13/cobra"
"github.com/steadybit/cli/v6/internal/environment"
"github.com/steadybit/cli/v6/internal/gitops"
"github.com/steadybit/cli/v6/internal/platform"
"github.com/steadybit/cli/v6/internal/resource"
)
Expand Down Expand Up @@ -48,6 +49,7 @@ func newEnvironment() *cobra.Command {
RunE: withClient(func(ctx context.Context, c *platform.Client, _ []string) error { return environment.Apply(ctx, c, a) }),
}
fileFlags(apply, &a.Files, &a.Recursive, "environment")
dryRun(apply, gitops.Environment, &a.Files, &a.Recursive)

var d environment.DeleteOptions
del := &cobra.Command{
Expand Down Expand Up @@ -90,6 +92,6 @@ func newEnvironment() *cobra.Command {
vset.Flags().BoolVar(&vs.Replace, "replace", false, "Remove every variable not given.")
variable.AddCommand(vget, vset)

cmd.AddCommand(list, get, apply, del, variable)
cmd.AddCommand(list, get, apply, newDiff(gitops.Environment, "environment", "environment.yml", "environments"), del, variable)
return cmd
}
2 changes: 1 addition & 1 deletion internal/cli/experiment.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
func newExperiment() *cobra.Command {
cmd := &cobra.Command{Use: "experiment", Short: "Check and run experiments."}
cmd.AddCommand(newExperimentRun(), newExperimentGet(), newExperimentApply(), newExperimentDelete(), newExperimentDump(), newExperimentInit(), newExperimentBadge(),
newDiff(gitops.Experiment, "experiment", "experiment.yml"))
newDiff(gitops.Experiment, "experiment", "experiment.yml", "experiments"))
return cmd
}

Expand Down
16 changes: 10 additions & 6 deletions internal/cli/gitops.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
)

// newDiff is the `diff` command of a kind of file: `experiment diff`, `schedule diff`...
func newDiff(k gitops.Kind, group, example string) *cobra.Command {
func newDiff(k gitops.Kind, group, example, dir string) *cobra.Command {
var files []string
var recursive bool
cmd := &cobra.Command{
Expand All @@ -22,7 +22,7 @@ func newDiff(k gitops.Kind, group, example string) *cobra.Command {
Args: cobra.NoArgs,
Example: examples(
"steadybit "+group+" diff -f "+example,
"steadybit "+group+" diff -f ./"+group+"s -R || echo drift",
"steadybit "+group+" diff -f ./"+dir+" -R || echo drift",
),
RunE: withClient(func(ctx context.Context, c *platform.Client, _ []string) error {
return gitops.DiffFiles(ctx, c, k, files, recursive)
Expand Down Expand Up @@ -57,28 +57,32 @@ func newExport() *cobra.Command {
var o gitops.ExportOptions
cmd := &cobra.Command{
Use: "export",
Short: "Write a team's experiments, schedules, services and the custom service profiles they use to a directory, to keep in Git.",
Short: "Write a team's experiments, schedules, services and the custom service profiles they use to a directory, to keep in Git. With --tenant, write the tenant's experiment templates, environments, teams, property definitions, hubs, integrations and custom service profiles instead.",
Args: cobra.NoArgs,
Example: examples(
"steadybit export --team ADM -d ./chaos",
"steadybit export --tenant -d ./platform",
),
RunE: withClient(func(ctx context.Context, c *platform.Client, _ []string) error { return gitops.Export(ctx, c, o) }),
}
cmd.Flags().StringVar(&o.Team, "team", "", "The key of the team to export.")
cmd.Flags().BoolVar(&o.Tenant, "tenant", false, "Export the configuration of the tenant rather than a team. Credentials of integrations are written masked.")
cmd.Flags().StringVarP(&o.Directory, "directory", "d", ".", "The directory to write the project to.")
_ = cmd.MarkFlagRequired("team")
cmd.MarkFlagsOneRequired("team", "tenant")
cmd.MarkFlagsMutuallyExclusive("team", "tenant")
return cmd
}

func newApplyProject() *cobra.Command {
var o gitops.ApplyOptions
cmd := &cobra.Command{
Use: "apply",
Short: "Apply a project written by `export`: service profiles, then services, experiments and schedules.",
Short: "Apply a project written by `export`: property definitions, environments, teams, hubs, experiment templates, integrations and service profiles, then services, experiments and schedules.",
Args: cobra.NoArgs,
Example: examples(
"steadybit apply -d ./chaos --dry-run",
"steadybit apply -d ./chaos",
"steadybit apply -d ./platform",
),
RunE: withClient(func(ctx context.Context, c *platform.Client, _ []string) error { return gitops.ApplyProject(ctx, c, o) }),
}
Expand All @@ -94,7 +98,7 @@ func newDiffProject() *cobra.Command {
Use: "diff",
Short: "Show how a project written by `export` differs from the platform. Exits with 2 when it does.",
Args: cobra.NoArgs,
Example: examples("steadybit diff -d ./chaos"),
Example: examples("steadybit diff -d ./chaos", "steadybit diff -d ./platform"),
RunE: withClient(func(ctx context.Context, c *platform.Client, _ []string) error {
return gitops.DiffProject(ctx, c, dir)
}),
Expand Down
4 changes: 3 additions & 1 deletion internal/cli/hub.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"

"github.com/spf13/cobra"
"github.com/steadybit/cli/v6/internal/gitops"
"github.com/steadybit/cli/v6/internal/hub"
"github.com/steadybit/cli/v6/internal/platform"
"github.com/steadybit/cli/v6/internal/resource"
Expand Down Expand Up @@ -45,6 +46,7 @@ func newHub() *cobra.Command {
RunE: withClient(func(ctx context.Context, c *platform.Client, _ []string) error { return hub.Apply(ctx, c, a) }),
}
fileFlags(apply, &a.Files, &a.Recursive, "hub")
dryRun(apply, gitops.Hub, &a.Files, &a.Recursive)
apply.Flags().BoolVar(&a.Synchronize, "synchronize", false, "Fetch the hub's templates from its repository, waiting until it is done.")

var d hub.DeleteOptions
Expand All @@ -69,6 +71,6 @@ func newHub() *cobra.Command {
}
idFlag(resync, &resyncID, "The hub id.")

cmd.AddCommand(list, get, apply, del, resync)
cmd.AddCommand(list, get, apply, newDiff(gitops.Hub, "hub", "hub.yml", "hubs"), del, resync)
return cmd
}
4 changes: 3 additions & 1 deletion internal/cli/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"

"github.com/spf13/cobra"
"github.com/steadybit/cli/v6/internal/gitops"
"github.com/steadybit/cli/v6/internal/integration"
"github.com/steadybit/cli/v6/internal/platform"
"github.com/steadybit/cli/v6/internal/resource"
Expand Down Expand Up @@ -67,6 +68,7 @@ func newIntegrationKind(k integration.Kind) *cobra.Command {
}),
}
fileFlags(apply, &a.Files, &a.Recursive, lower)
dryRun(apply, gitops.Integrations[k.Name], &a.Files, &a.Recursive)

var d integration.DeleteOptions
del := &cobra.Command{
Expand All @@ -81,6 +83,6 @@ func newIntegrationKind(k integration.Kind) *cobra.Command {
idFlag(del, &d.ID, "The "+lower+" id.")
del.Flags().BoolVar(&d.Yes, "yes", false, yesHelp)

cmd.AddCommand(list, get, apply, del)
cmd.AddCommand(list, get, apply, newDiff(gitops.Integrations[k.Name], "integration "+k.Name, k.Name+".yml", "integrations/"+k.Name), del)
return cmd
}
4 changes: 3 additions & 1 deletion internal/cli/property.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"

"github.com/spf13/cobra"
"github.com/steadybit/cli/v6/internal/gitops"
"github.com/steadybit/cli/v6/internal/platform"
"github.com/steadybit/cli/v6/internal/property"
"github.com/steadybit/cli/v6/internal/resource"
Expand Down Expand Up @@ -65,6 +66,7 @@ func newPropertyDefinition() *cobra.Command {
}
fileFlags(apply, &a.Files, &a.Recursive, "property definition")
apply.Flags().BoolVar(&a.DeleteValues, "delete-values", false, "Allow removing enum values still in use, deleting them where they are used.")
dryRun(apply, gitops.PropertyDefinition, &a.Files, &a.Recursive)

var d property.DeleteDefinitionOptions
del := &cobra.Command{
Expand All @@ -80,7 +82,7 @@ func newPropertyDefinition() *cobra.Command {
del.Flags().BoolVar(&d.Associations, "delete-associations", false, "Also delete the associations of the property.")
del.Flags().BoolVar(&d.Yes, "yes", false, yesHelp)

cmd.AddCommand(list, get, apply, del)
cmd.AddCommand(list, get, apply, newDiff(gitops.PropertyDefinition, "property definition", "result-color.yml", "property-definitions"), del)
return cmd
}

Expand Down
2 changes: 1 addition & 1 deletion internal/cli/schedule.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ func newSchedule() *cobra.Command {
scheduleIDFlag(c, &id)
return c
}
cmd.AddCommand(list, get, apply, newDiff(gitops.Schedule, "schedule", "schedule.yml"), create, update,
cmd.AddCommand(list, get, apply, newDiff(gitops.Schedule, "schedule", "schedule.yml", "schedules"), create, update,
idCommand("enable", "Enable an experiment schedule.", func(ctx context.Context, c *platform.Client, id string) error {
return schedule.SetEnabled(ctx, c, id, true)
}),
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ func newService() *cobra.Command {
vset.Flags().BoolVar(&vs.Replace, "replace", false, "Remove every variable not given.")
variable.AddCommand(vget, vset)

cmd.AddCommand(list, get, apply, newDiff(gitops.Service, "service", "service.yml"), del, risk, experiments, variable)
cmd.AddCommand(list, get, apply, newDiff(gitops.Service, "service", "service.yml", "services"), del, risk, experiments, variable)
return cmd
}

Expand Down Expand Up @@ -260,6 +260,6 @@ func newServiceProfile() *cobra.Command {
}
idFlag(del, &deleteID, "The service profile id.")

cmd.AddCommand(list, get, apply, newDiff(gitops.ServiceProfile, "service-profile", "profile.yml"), del)
cmd.AddCommand(list, get, apply, newDiff(gitops.ServiceProfile, "service-profile", "profile.yml", "service-profiles"), del)
return cmd
}
4 changes: 3 additions & 1 deletion internal/cli/team.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"

"github.com/spf13/cobra"
"github.com/steadybit/cli/v6/internal/gitops"
"github.com/steadybit/cli/v6/internal/platform"
"github.com/steadybit/cli/v6/internal/resource"
"github.com/steadybit/cli/v6/internal/team"
Expand Down Expand Up @@ -51,6 +52,7 @@ func newTeam() *cobra.Command {
RunE: withClient(func(ctx context.Context, c *platform.Client, _ []string) error { return team.Apply(ctx, c, a) }),
}
fileFlags(apply, &a.Files, &a.Recursive, "team")
dryRun(apply, gitops.Team, &a.Files, &a.Recursive)

var d team.DeleteOptions
del := &cobra.Command{
Expand All @@ -64,7 +66,7 @@ func newTeam() *cobra.Command {
del.Flags().BoolVar(&d.Experiments, "purge-experiments", false, "Also delete the team's experiments and their runs.")
del.Flags().BoolVar(&d.Yes, "yes", false, yesHelp)

cmd.AddCommand(list, get, apply, del, newTeamMember(), newTeamEnvironment())
cmd.AddCommand(list, get, apply, newDiff(gitops.Team, "team", "team.yml", "teams"), del, newTeamMember(), newTeamEnvironment())
return cmd
}

Expand Down
4 changes: 3 additions & 1 deletion internal/cli/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"

"github.com/spf13/cobra"
"github.com/steadybit/cli/v6/internal/gitops"
"github.com/steadybit/cli/v6/internal/platform"
"github.com/steadybit/cli/v6/internal/resource"
"github.com/steadybit/cli/v6/internal/template"
Expand Down Expand Up @@ -80,6 +81,7 @@ func newTemplate() *cobra.Command {
RunE: withClient(func(ctx context.Context, c *platform.Client, _ []string) error { return template.Apply(ctx, c, a) }),
}
fileFlags(apply, &a.Files, &a.Recursive, "template")
dryRun(apply, gitops.Template, &a.Files, &a.Recursive)

var d template.DeleteOptions
del := &cobra.Command{
Expand Down Expand Up @@ -107,6 +109,6 @@ func newTemplate() *cobra.Command {
_ = imp.MarkFlagRequired("template")
variadic(imp, "template")

cmd.AddCommand(list, get, apply, del, imp)
cmd.AddCommand(list, get, apply, newDiff(gitops.Template, "template", "template.yml", "templates"), del, imp)
return cmd
}
6 changes: 3 additions & 3 deletions internal/environment/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (

// The state is the platform's, and the version is dropped as `service get` drops it: kept
// in a file, it turns every apply after an edit in the UI into a conflict.
var readOnly = []string{"version", "state"}
var ReadOnly = []string{"version", "state"}

func uuid(id string) (openapi_types.UUID, error) {
u, ok := resource.UUID(id)
Expand Down Expand Up @@ -88,7 +88,7 @@ func Get(ctx context.Context, c *platform.Client, o GetOptions) error {
if err != nil {
return notFoundOr(err, o.ID, "Failed to get environment %s")
}
if err := resource.Output(resource.Strip(doc, readOnly...), o.File, o.Type); err != nil {
if err := resource.Output(resource.Strip(doc, ReadOnly...), o.File, o.Type); err != nil {
return err
}
if o.File != "" {
Expand All @@ -109,7 +109,7 @@ func Apply(ctx context.Context, c *platform.Client, o ApplyOptions) error {
return resource.Applied{}, fmt.Errorf("Environment file '%s' does not name the environment.", file)
}
var saved struct{ ID, Name string }
resp, err := c.UpsertEnvironmentWithBody(ctx, "application/json", resource.Body(resource.Strip(doc, readOnly...).Value()))
resp, err := c.UpsertEnvironmentWithBody(ctx, "application/json", resource.Body(resource.Strip(doc, ReadOnly...).Value()))
resp, err = platform.Decode(resp, err, &saved)
if err != nil {
return resource.Applied{}, platform.Failed(err, "Failed to save environment %s", name)
Expand Down
Loading
Loading