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
11 changes: 10 additions & 1 deletion cmd/deploy/gitops.go
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,12 @@ func printPublishPlan(plan *gitops.PublishPlan) {
for _, removal := range plan.CellRemovals {
cli.Warning("The cell file removes %s, as --allow-cell-removal names", removal)
}
for _, removal := range plan.UndeclaredCellRemovals {
cli.Warning("The cell file removes %s", removal)
}
for _, removed := range plan.RemovedModules {
cli.Warning("Removed %s from the delivery: the composition no longer declares it", removed)
}
for _, stale := range plan.StaleConsumers {
cli.Warning("Delivered consumer %s bakes in another value of a workspace configuration group this publication carries; publish it next, so %s does not keep delivering two values of one group", stale, plan.Environment)
}
Expand Down Expand Up @@ -657,7 +663,7 @@ func init() {
}
for _, command := range []*cobra.Command{gitOpsPlanCmd, gitOpsPublishCmd} {
command.Flags().StringSliceVar(&gitOpsAllowCellRemovals, "allow-cell-removal", nil,
"Module whose removal from the environment's cell file, relative to the base branch's, is intended (repeatable); any other removal refuses")
"Module whose removal from the environment's cell file, relative to the base branch's, is intended (repeatable); a module the composition no longer declares is removed without it, and any other removal refuses")
}
for _, command := range []*cobra.Command{gitOpsPublishCmd, gitOpsRollbackCmd} {
command.Flags().StringVar(&gitOpsMessage, "message", "", "Signed commit message")
Expand Down Expand Up @@ -696,6 +702,9 @@ func printCell(cell *gitops.CellResult) {
if len(cell.Skipped) > 0 {
cli.Info("Cell file leaves out %s: rendered for another environment", strings.Join(cell.Skipped, ", "))
}
for _, removed := range cell.Removed {
cli.Info("removed %s: the composition no longer declares it", removed)
}
if !cell.Source {
cli.Info("Cell file names no source commit: the environment declares no gitops repository and branch, or the workspace is not a git checkout")
}
Expand Down
4 changes: 2 additions & 2 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2274,7 +2274,7 @@ codefly deploy gitops plan [module]
Flags:

```
--allow-cell-removal strings Module whose removal from the environment's cell file, relative to the base branch's, is intended (repeatable); any other removal refuses
--allow-cell-removal strings Module whose removal from the environment's cell file, relative to the base branch's, is intended (repeatable); a module the composition no longer declares is removed without it, and any other removal refuses
--allow-unresolved-contracts Downgrade a consumed contract whose exposing module is not yet deployed to a warning, for bootstrap ordering
--env string Environment to promote (default "local")
--local Use a disposable local file Git remote for k3d qualification
Expand All @@ -2294,7 +2294,7 @@ codefly deploy gitops publish [module]
Flags:

```
--allow-cell-removal strings Module whose removal from the environment's cell file, relative to the base branch's, is intended (repeatable); any other removal refuses
--allow-cell-removal strings Module whose removal from the environment's cell file, relative to the base branch's, is intended (repeatable); a module the composition no longer declares is removed without it, and any other removal refuses
--allow-unresolved-contracts Downgrade a consumed contract whose exposing module is not yet deployed to a warning, for bootstrap ordering
--body string Promotion pull request body
--env string Environment to promote (default "local")
Expand Down
46 changes: 43 additions & 3 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -1150,6 +1150,16 @@ and egress the environment's), so an entry carried over is a claim about a
composition that may no longer exist, and no digest is invented for a module
that was not rendered.

The file is derived from the **composition**, not from whatever records the
checkout holds. A module removed from `modules:` leaves its render record
behind, and so does one that was never composed here; such a record is left
out of the file and deleted by the render that derives it — nothing could
render that module again — and the render says so, one line per record
(`removed deployments/modules/<module>: the composition no longer declares
it`). Every render that re-derives the cell file prunes this way, a single
module's and `--all` alike, because that is where the claim about the cell is
made; an environment with no `host:` has no cell file and nothing is pruned.

**A publish carries the cell file.** A cell admits only the builds its cell
file names, so a promotion that delivered a module's tree and left the file
behind would deliver builds the cluster refuses — every refused workload
Expand Down Expand Up @@ -1188,9 +1198,39 @@ committed with the rendered trees, and a cell admits only what it names, so
and the one the delivery base branch carries at
`<gitops path>/cells/<environment>/cell.yaml`, and refuse when the workspace's
removes a namespace, a workload, an endpoint or a consumer of an endpoint the
base admits — naming each removal and the modules it concerns. A removal that
is intended (a module the workspace no longer composes, a dependency dropped)
is named with `--allow-cell-removal <module>` (repeatable); a consumer edge is
base admits — naming each removal and the modules it concerns. A removal the
composition explains needs no naming: when the module it concerns is no
longer in `modules:` (the namespace of a module removed from the composition,
or its consumer edge on another module's endpoint), **and the checkout has
seen the composition the base's file was rendered from**, the hold lets it
through and the plan reports it (`undeclaredCellRemovals`), and the
publication also removes that module's delivered tree from
`<gitops path>/modules/` with everything else it writes (`removedModules`):
the cell no longer admits anything of it, and the ApplicationSet that tree
carries would keep stamping Applications whose every pod the cell refuses. A
checkout that never had a module and one that dropped it derive the same cell
file, so the history decides: when the workspace repository is the delivery
repository (it holds the base commit, or the delivery holds the workspace
revision the base's file names as its `source.revision`), the checkout must
contain the delivery base commit itself — the file's source revision is the
render's parent, and a `modules:` edit rides in the render's own commit, so an
unfetched checkout at that parent has not seen it; when the two repositories
are separate, the checkout must contain that source revision, and the
workspace's own configuration **as committed at that revision** must have
declared the module (a render may run on an uncommitted `modules:` edit, and
a module of an imported workspace is another repository's history), or
nothing in this history vouches the module was removed rather than never
composed. A checkout that fails either test — a branch behind the base, an
unfetched one, a base file naming no source, a composition never committed —
is held as before, the refusal says what the history lacked, and naming the
module confirms the removal without pruning the tree. A promotion
of a module owns, on its own branch, the removal of such trees, so continuing
or retrying the publication that removed one is not refused as someone else's
change. A rollback removes none, and a directory under the modules path with
no render record is not a delivery and is left alone. Any other removal — a
module still composed whose render is missing from the checkout, a dependency
dropped, a service withdrawn — is
named with `--allow-cell-removal <module>` (repeatable); a consumer edge is
covered by naming either the endpoint's module or the consumer's, and the plan
lists every removal it lets through. A base with no cell file has nothing to
hold the workspace's to. The file held is the file carried — the same bytes,
Expand Down
70 changes: 68 additions & 2 deletions pkg/gitops/cell.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ type CellResult struct {
// Skipped names module trees rendered for another environment, which
// describe another cell.
Skipped []string
// Removed are the render records of modules the composition no longer
// declares, deleted by this render (pruneUndeclaredRecords), as paths
// relative to the workspace.
Removed []string
// Source reports whether the file names its repository, branch and base
// revision. It does not when the environment declares no gitops
// repository or the workspace is not a git checkout.
Expand Down Expand Up @@ -90,16 +94,78 @@ func CellPath(workspace *resources.Workspace, env *environments.Environment) (st
// composition that produced them, and writes it. It runs after a module
// render, so the file always describes the trees as they stand.
func RenderCell(ctx context.Context, workspace *resources.Workspace, env *environments.Environment) (CellResult, error) {
removed, err := pruneUndeclaredRecords(workspace)
if err != nil {
return CellResult{}, err
}
file, result, err := deriveCell(ctx, workspace, env)
if err != nil {
return CellResult{}, err
}
if err := writeCell(&file, result.Path); err != nil {
if err = writeCell(&file, result.Path); err != nil {
return CellResult{}, err
}
result.Removed = removed
return result, nil
}

// pruneUndeclaredRecords deletes, before the cell file is derived, the render
// records of modules the composition no longer declares, for any environment.
// Nothing could render such a module again (a render refuses a module the
// workspace does not compose), so its record describes a composition that no
// longer exists; left in place it was read back into every cell file derived
// here, and a module removed from `modules:` kept its namespace, workloads and
// consumer edges in the cell. The derivation's own hold on such a tree
// (refuseUncomposedRender) stays what it is: a render satisfies it by removing
// the tree, which is what that refusal asks for, and answers the paths it
// removed, relative to the workspace, for the render to print. A record that
// cannot be read is left for the derivation to name.
func pruneUndeclaredRecords(workspace *resources.Workspace) ([]string, error) {
modulesDir := filepath.Dir(moduleRenderDestination(workspace, "module"))
entries, err := os.ReadDir(modulesDir)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil, nil
}
return nil, fmt.Errorf("list rendered modules: %w", err)
}
declared := composedModules(workspace)
var removed []string
for _, entry := range entries {
if !entry.IsDir() {
continue
}
tree := filepath.Join(modulesDir, entry.Name())
inventory, loadErr := LoadInventory(tree)
if loadErr != nil || declared[inventory.Module] {
continue
}
if err = os.RemoveAll(tree); err != nil {
return nil, fmt.Errorf("remove the render record of module %s, which the composition no longer declares: %w", inventory.Module, err)
}
relative, relErr := filepath.Rel(workspace.Dir(), tree)
if relErr != nil {
relative = tree
}
removed = append(removed, filepath.ToSlash(relative))
}
sort.Strings(removed)
return removed, nil
}

// composedModules names every module the composition declares, in this
// workspace or one it imports: the set a cell file describes and the set a
// removal from it is judged against.
func composedModules(workspace *resources.Workspace) map[string]bool {
declared := make(map[string]bool, len(workspace.Modules))
for _, reference := range workspace.Modules {
if reference != nil {
declared[reference.Name] = true
}
}
return declared
}

// deriveCell is RenderCell without the write: the cell file the rendered
// trees and the composition describe, and the path it belongs at.
func deriveCell(ctx context.Context, workspace *resources.Workspace, env *environments.Environment) (cellfile.File, CellResult, error) {
Expand Down Expand Up @@ -156,7 +222,7 @@ func deriveCell(ctx context.Context, workspace *resources.Workspace, env *enviro
result.Skipped = append(result.Skipped, inventory.Module)
continue
}
if err := refuseUncomposedRender(workspace, env, tree, &inventory); err != nil {
if err = refuseUncomposedRender(workspace, env, tree, &inventory); err != nil {
return cellfile.File{}, CellResult{}, err
}
// A tree rendered before the environment excluded one of its services
Expand Down
Loading
Loading