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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes

.PHONY: build
build: manifests generate fmt vet ## Build manager binary.
go build -o bin/manager main.go
go build -o bin/manager cmd/main.go

.PHONY: run
run: manifests generate fmt vet ## Run a controller from your host.
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,25 @@ make run
> **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin
privileges or be logged in as admin.

## Namespace Prefix Configuration

The operator creates namespaces for each stack with the format `<organisationID>-<stackID>`. You can configure a prefix to be added to all namespace names by using the `--namespace-prefix` command line flag.

**Example:**
- Without prefix: `myorg-mystack`
- With prefix `dev-`: `dev-myorg-mystack`

**Command line usage:**
```sh
make run -- --namespace-prefix=dev-
```

**Helm chart usage:**
```yaml
operator:
namespacePrefix: "dev-"
```
Comment on lines +132 to +149
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Add migration guidance for existing deployments.

The documentation doesn't address what happens to existing stacks when the namespace prefix is added or changed. When the prefix is enabled on an existing operator deployment, new namespaces will be created rather than renaming existing ones, potentially leaving orphaned resources in the old namespaces.

Consider adding a warning and migration instructions for existing deployments.

Additionally, the documentation states the format is <organisationID>-<stackID>, but the code uses stackName directly. Can you clarify the relationship between these concepts? If stackName already includes the organization ID, the documentation should reflect this.


I'll verify the review comment by examining the codebase for the namespace prefix implementation and how it handles existing deployments.

I'll verify the review comment by examining the codebase for the namespace prefix implementation and how it handles existing deployments.


🏁 Script executed:

#!/bin/bash
# Search for namespace prefix implementation in the codebase
echo "=== Searching for namespace prefix logic ==="
rg -i "namespace.*prefix" -A 5 -B 5 --type=go --type=yaml --type=ts --type=js

echo ""
echo "=== Searching for namespace creation logic ==="
rg -nP '\bnamespace\b.*creation|createNamespace|namespace.*format' -A 3 -B 3 --type=go --type=yaml

echo ""
echo "=== Searching for stackName usage ==="
rg -nP '\bstackName\b' -A 2 -B 2 --type=go --type=ts

echo ""
echo "=== Searching for organisationID references ==="
rg -nP '\borganis.*ID|organisationID' -A 2 -B 2 --type=go --type=yaml

echo ""
echo "=== Checking for migration or upgrade documentation ==="
find . -name "*.md" -type f | head -20 | xargs grep -l -i "migrat\|upgrad\|breaking\|orphan" 2>/dev/null | head -10

Length of output: 21345


🏁 Script executed:

#!/bin/bash
# Search for namespace prefix implementation in the codebase
echo "=== Searching for namespace prefix logic ==="
rg -i "namespace.*prefix" -A 5 -B 5 --type=go --type=yaml --type=ts --type=js

echo ""
echo "=== Searching for namespace creation logic ==="
rg -nP '\bnamespace\b.*creation|createNamespace|namespace.*format' -A 3 -B 3 --type=go --type=yaml

echo ""
echo "=== Searching for stackName usage ==="
rg -nP '\bstackName\b' -A 2 -B 2 --type=go --type=ts

echo ""
echo "=== Searching for organisationID references ==="
rg -nP '\borganis.*ID|organisationID' -A 2 -B 2 --type=go --type=yaml

echo ""
echo "=== Checking for migration or upgrade documentation ==="
find . -name "*.md" -type f | head -20 | xargs grep -l -i "migrat\|upgrad\|breaking\|orphan" 2>/dev/null | head -10

Length of output: 21345


🏁 Script executed:

#!/bin/bash
# Search for namespace deletion/cleanup logic when prefix changes
echo "=== Searching for namespace cleanup or deletion logic ==="
rg -nP 'DeleteNamespace|delete.*namespace|cleanup' -A 3 -B 3 --type=go

echo ""
echo "=== Check README.md around lines 132-149 ==="
sed -n '120,160p' README.md

Length of output: 1508


Clarify namespace format documentation and add migration guidance for prefix changes.

The codebase confirms both concerns:

  1. Migration guidance is missing: When the namespace prefix is added or changed on an existing deployment, the operator will create new namespaces for stacks (e.g., dev-myorg-mystack) while leaving the original namespaces (e.g., myorg-mystack) orphaned and unmanaged. There is no cleanup or migration logic to handle this scenario.

  2. Documentation format is misleading: The documentation states the format is <organisationID>-<stackID> as separate components, but the code implementation (in internal/core/names.go) treats stackName as an atomic value that already contains the full identifier. The format description should clarify that stackName is already in the form organisation-stack.

Update README to:

  • Warn that enabling or changing the prefix on existing deployments will create new namespaces, leaving old resources orphaned
  • Provide migration steps for existing deployments (e.g., instructions to manually migrate resources or guidelines on coordinating the upgrade)
  • Clarify that the namespace format is <prefix><stackName> where stackName includes both organization and stack identifiers
🤖 Prompt for AI Agents
In README.md around lines 132 to 149, update the Namespace Prefix Configuration
section to (1) warn that enabling or changing --namespace-prefix (or
operator.namespacePrefix) on an existing deployment will cause the operator to
create new namespaces like <prefix><stackName> and leave previous namespaces
orphaned, (2) add concise migration guidance: recommended upgrade order, example
manual migration steps (move/replicate resources, update CRs, rebind services)
and a note to coordinate downtime/backups, and (3) clarify the namespace format
wording to state explicitly that the operator builds namespaces as
<prefix><stackName> where stackName is an atomic value that already contains
organisation and stack identifiers (e.g., stackName="myorg-mystack" → namespace
"dev-myorg-mystack"); keep examples for CLI and Helm usage and a short reminder
to plan cleanup of old namespaces.


**Create instances of your solution**
You can apply the samples (examples) from the config/sample:

Expand Down
11 changes: 7 additions & 4 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func main() {
env string
licenceSecret string
utilsVersion string
namespacePrefix string
)
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
Expand All @@ -77,6 +78,7 @@ func main() {
flag.StringVar(&env, "env", "staging", "The current environment in use for the operator")
flag.StringVar(&licenceSecret, "licence-secret", "", "The licence secret that contains the token and the issuer")
flag.StringVar(&utilsVersion, "utils-version", "latest", "The version of the operator utils image")
flag.StringVar(&namespacePrefix, "namespace-prefix", "", "The prefix to use when creating namespaces (format: <prefix><organisationID>-<stackID>)")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use CRD Settings instead of Flag on Operator

opts := zap.Options{
Development: false,
}
Expand Down Expand Up @@ -119,10 +121,11 @@ func main() {
}

platform := core.Platform{
Region: region,
Environment: env,
LicenceSecret: licenceSecret,
UtilsVersion: utilsVersion,
Region: region,
Environment: env,
LicenceSecret: licenceSecret,
UtilsVersion: utilsVersion,
NamespacePrefix: namespacePrefix,
}

if err := core.Setup(mgr, platform); err != nil {
Expand Down
3 changes: 3 additions & 0 deletions helm/operator/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ spec:
{{- with .Values.operator.region }}
- --region={{ $.Values.operator.region }}
{{- end }}
{{- with .Values.operator.namespacePrefix }}
- --namespace-prefix={{ $.Values.operator.namespacePrefix }}
{{- end }}
{{- if and .Values.global.licence.createSecret (eq (len .Values.global.licence.existingSecret) 0) }}
- --licence-secret={{ include "operator.fullname" . }}-licence
{{- else }}
Expand Down
2 changes: 2 additions & 0 deletions helm/operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ operator:
probeAddr: ":8081"
# Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.
enableLeaderElection: true
# The namespace prefix to use when creating namespaces (format: <prefix><organisationID>-<stackID>)
namespacePrefix: ""

utils:
tag: ""
Expand Down
17 changes: 17 additions & 0 deletions internal/core/names.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,20 @@ func GetResourceName(name string) types.NamespacedName {
Name: name,
}
}

// GetNamespaceName generates a namespace name with optional prefix
// Format: <prefix><organisationID>-<stackID> or <organisationID>-<stackID> if no prefix
func GetNamespaceName(platform Platform, stackName string) string {
if platform.NamespacePrefix == "" {
return stackName
}
return platform.NamespacePrefix + stackName
}

// GetNamespacedResourceNameWithPrefix generates a namespaced resource name using the prefixed namespace
func GetNamespacedResourceNameWithPrefix(platform Platform, stackName, name string) types.NamespacedName {
return types.NamespacedName{
Namespace: GetNamespaceName(platform, stackName),
Name: name,
}
}
2 changes: 2 additions & 0 deletions internal/core/platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ type Platform struct {
LicenceSecret string
// The operator utils image version
UtilsVersion string
// The namespace prefix to use when creating namespaces
NamespacePrefix string
}
2 changes: 1 addition & 1 deletion internal/resources/benthos/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ func createDeployment(ctx Context, stack *v1beta1.Stack, b *v1beta1.Benthos) err

object := &unstructured.Unstructured{}
object.SetGroupVersionKind(kinds[0])
object.SetNamespace(stack.Name)
object.SetNamespace(GetNamespaceName(ctx.GetPlatform(), stack.Name))
object.SetName("benthos-audit")
if err := client.IgnoreNotFound(ctx.GetClient().Delete(ctx, object)); err != nil {
return errors.Wrap(err, "deleting audit config map")
Expand Down
8 changes: 4 additions & 4 deletions internal/resources/ledgers/deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func hasDeploymentStrategyChanged(ctx core.Context, stack *v1beta1.Stack, ledger
case v1beta1.DeploymentStrategySingle:
return uninstallLedgerMonoWriterMultipleReader(ctx, stack)
case v1beta1.DeploymentStrategyMonoWriterMultipleReader:
return core.DeleteIfExists[*appsv1.Deployment](ctx, core.GetNamespacedResourceName(stack.Name, "ledger"))
return core.DeleteIfExists[*appsv1.Deployment](ctx, core.GetNamespacedResourceNameWithPrefix(ctx.GetPlatform(), stack.Name, "ledger"))
default:
return fmt.Errorf("unknown deployment strategy %s", strategy)
}
Expand Down Expand Up @@ -385,10 +385,10 @@ func installLedgerMonoWriterMultipleReader(ctx core.Context, stack *v1beta1.Stac
func uninstallLedgerMonoWriterMultipleReader(ctx core.Context, stack *v1beta1.Stack) error {

remove := func(name string) error {
if err := core.DeleteIfExists[*appsv1.Deployment](ctx, core.GetNamespacedResourceName(stack.Name, name)); err != nil {
if err := core.DeleteIfExists[*appsv1.Deployment](ctx, core.GetNamespacedResourceNameWithPrefix(ctx.GetPlatform(), stack.Name, name)); err != nil {
return err
}
if err := core.DeleteIfExists[*corev1.Service](ctx, core.GetNamespacedResourceName(stack.Name, name)); err != nil {
if err := core.DeleteIfExists[*corev1.Service](ctx, core.GetNamespacedResourceNameWithPrefix(ctx.GetPlatform(), stack.Name, name)); err != nil {
return err
}

Expand All @@ -403,7 +403,7 @@ func uninstallLedgerMonoWriterMultipleReader(ctx core.Context, stack *v1beta1.St
return err
}

if err := core.DeleteIfExists[*appsv1.Deployment](ctx, core.GetNamespacedResourceName(stack.Name, "ledger-gateway")); err != nil {
if err := core.DeleteIfExists[*appsv1.Deployment](ctx, core.GetNamespacedResourceNameWithPrefix(ctx.GetPlatform(), stack.Name, "ledger-gateway")); err != nil {
return err
}

Expand Down
4 changes: 2 additions & 2 deletions internal/resources/payments/deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,10 @@ func commonEnvVars(ctx core.Context, stack *v1beta1.Stack, payments *v1beta1.Pay

func uninstallPaymentsReadAndConnectors(ctx core.Context, stack *v1beta1.Stack) error {
remove := func(name string) error {
if err := core.DeleteIfExists[*appsv1.Deployment](ctx, core.GetNamespacedResourceName(stack.Name, name)); err != nil {
if err := core.DeleteIfExists[*appsv1.Deployment](ctx, core.GetNamespacedResourceNameWithPrefix(ctx.GetPlatform(), stack.Name, name)); err != nil {
return err
}
if err := core.DeleteIfExists[*corev1.Service](ctx, core.GetNamespacedResourceName(stack.Name, name)); err != nil {
if err := core.DeleteIfExists[*corev1.Service](ctx, core.GetNamespacedResourceNameWithPrefix(ctx.GetPlatform(), stack.Name, name)); err != nil {
return err
}

Expand Down
4 changes: 2 additions & 2 deletions internal/resources/resourcereferences/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func Reconcile(ctx core.Context, stack *v1beta1.Stack, req *v1beta1.ResourceRefe
oldResource := &unstructured.Unstructured{}
oldResource.SetGroupVersionKind(gvk)
err := ctx.GetClient().Get(ctx, types.NamespacedName{
Namespace: stack.Name,
Namespace: core.GetNamespaceName(ctx.GetPlatform(), stack.Name),
Name: req.Status.SyncedResource,
}, oldResource)
if client.IgnoreNotFound(err) != nil {
Expand Down Expand Up @@ -147,7 +147,7 @@ func Reconcile(ctx core.Context, stack *v1beta1.Stack, req *v1beta1.ResourceRefe

newResource := &unstructured.Unstructured{}
newResource.SetGroupVersionKind(gvk)
newResource.SetNamespace(stack.Name)
newResource.SetNamespace(core.GetNamespaceName(ctx.GetPlatform(), stack.Name))
newResource.SetName(req.Spec.Name)
Comment on lines +150 to 151
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify all namespace usages are updated.

The namespace resolution correctly uses GetNamespaceName when creating the new resource. However, we should verify that all locations in the codebase that use stack.Name for namespace resolution have been updated to use the platform-aware helper.

Run the following script to find potential locations where stack.Name might still be used for namespace resolution without the prefix:


🏁 Script executed:

#!/bin/bash
# Search for patterns where stack.Name might be used as a namespace
# Focus on SetNamespace, Namespace: stack.Name, and types.NamespacedName patterns

echo "=== Checking SetNamespace calls ==="
rg -n 'SetNamespace\(\s*stack\.Name\s*\)' --type go

echo "=== Checking Namespace field assignments ==="
rg -n 'Namespace:\s*stack\.Name' --type go

echo "=== Checking types.NamespacedName with stack.Name ==="
rg -n 'types\.NamespacedName\s*{[^}]*Namespace:\s*stack\.Name' --type go

echo "=== Checking direct stack.Name usage in namespace contexts ==="
# Look for GetClient().Get/Create/Update with stack.Name
rg -nC3 '(Get|Create|Update|Delete|List)\(ctx,.*stack\.Name' --type go | grep -v GetNamespaceName

Length of output: 1383



---

🏁 Script executed:

```shell
#!/bin/bash
# Find GetNamespaceName definition and understand the pattern
echo "=== GetNamespaceName definition ==="
rg -nA5 'func GetNamespaceName' --type go

echo ""
echo "=== Count of GetNamespaceName usages ==="
rg -n 'GetNamespaceName\(' --type go | wc -l

echo ""
echo "=== Sample usages of GetNamespaceName ==="
rg -nB2 'GetNamespaceName\(' --type go | head -30

Length of output: 2179


🏁 Script executed:

#!/bin/bash
# Check a few production code locations to understand context
echo "=== Context for ingress.go:129 ==="
rg -nB10 -A5 'Namespace: stack.Name' internal/resources/gateways/ingress.go | head -20

echo ""
echo "=== Context for authclients/init.go:54 ==="
rg -nB10 -A5 'Namespace: stack.Name' internal/resources/authclients/init.go | head -20

Length of output: 1000


Update namespace usage across multiple files to use GetNamespaceName helper consistently.

The reviewed file correctly uses GetNamespaceName, but verification found 5+ production code locations still using stack.Name directly for namespace resolution without the platform-aware prefix transformation:

  • internal/resources/gateways/ingress.go:129
  • internal/resources/gateways/configuration.go:19
  • internal/resources/caddy/caddy.go:153
  • internal/resources/auths/configuration.go:42
  • internal/resources/authclients/init.go:54
  • internal/resources/ledgers/init.go:115 (via client.InNamespace(stack.Name))

These must be updated to use core.GetNamespaceName(ctx.GetPlatform(), stack.Name) to ensure namespace prefix configuration is applied consistently across all resource creation/lookup operations.

🤖 Prompt for AI Agents
In internal/resources/gateways/ingress.go around line 129,
internal/resources/gateways/configuration.go around line 19,
internal/resources/caddy/caddy.go around line 153,
internal/resources/auths/configuration.go around line 42,
internal/resources/authclients/init.go around line 54, and
internal/resources/ledgers/init.go around line 115, update any direct use of
stack.Name for namespace resolution to call
core.GetNamespaceName(ctx.GetPlatform(), stack.Name) instead; replace
occurrences used for resource creation, lookups, and client.InNamespace(...) so
the platform-aware namespace prefix is applied consistently.


_, err = controllerutil.CreateOrUpdate(ctx, ctx.GetClient(), newResource, func() error {
Expand Down
6 changes: 5 additions & 1 deletion internal/resources/stacks/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,13 @@ func Reconcile(ctx Context, stack *v1beta1.Stack) error {
logger := log.FromContext(ctx)
logger = logger.WithValues("stack", stack.Name)
logger.Info("Reconcile stack")

// Generate namespace name with optional prefix
namespaceName := GetNamespaceName(ctx.GetPlatform(), stack.Name)

if _, _, err := CreateOrUpdate(ctx,
types.NamespacedName{
Name: stack.Name,
Name: namespaceName,
},
namespaceLabel(ctx, stack.Name),
namespaceAnnotations(ctx, stack.Name),
Expand Down