Skip to content
Merged
7 changes: 6 additions & 1 deletion pkg/clouds/pulumi/gcp/cloudsql_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,12 @@ func NewCloudsqlProxy(ctx *sdk.Context, args CloudSQLProxyArgs, opts ...sdk.Reso
}

opts = append(opts, sdk.Provider(args.KubeProvider))
// Sanitize the secret name to comply with Kubernetes 63-character limit
// args.Metadata.Namespace is the live Namespace.Metadata.Name() Output, threaded
// through by compute_proc.go (preprocessor uses kubeArgs.NamespaceNameOutput,
// postprocessor uses sc.Namespace). That means this Secret automatically lands
// in the same k8s namespace as the consuming pod under both fresh deploys (isolated
// name) and migrated stacks where #255's IgnoreChanges("metadata.name") keeps the
// Namespace parent-shared — no IgnoreChanges needed here.
secretName := util.SanitizeK8sResourceName(args.Name + "-creds")
sqlProxySecret, err := v1.NewSecret(ctx, secretName, &v1.SecretArgs{
Metadata: args.Metadata,
Expand Down
49 changes: 21 additions & 28 deletions pkg/clouds/pulumi/gcp/compute_proc.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,15 +215,20 @@ func appendUsesPostgresResourceContext(ctx *sdk.Context, params appendParams) er

func addCloudsqlProxySidecarPreProcessor(ctx *sdk.Context, params appendParams) {
params.collector.AddPreProcessor(&kubernetes.SimpleContainerArgs{}, func(arg any) error {
cloudsqlProxy, err := createCloudsqlProxy(ctx, params)
if err != nil {
return errors.Wrapf(err, "failed to create cloudsql proxy for %q in stack %q", params.postgresName, params.stack.Name)
}

kubeArgs, ok := arg.(*kubernetes.SimpleContainerArgs)
if !ok {
return errors.Errorf("arg is not *kubernetes.Args")
}
// Live Namespace name from the just-created Namespace resource. Using this
// Output (instead of recomputing via kubernetes.GenerateNamespaceName) keeps
// the CSQL credential Secret in lock-step with the Namespace under both
// fresh deploys (Output resolves to the isolated name) and migrated stacks
// where #255's IgnoreChanges("metadata.name") keeps the Namespace's k8s
// name parent-shared.
cloudsqlProxy, err := createCloudsqlProxy(ctx, params, kubeArgs.NamespaceNameOutput)
if err != nil {
return errors.Wrapf(err, "failed to create cloudsql proxy for %q in stack %q", params.postgresName, params.stack.Name)
}
kubeArgs.SidecarOutputs = append(kubeArgs.SidecarOutputs, cloudsqlProxy.ProxyContainer.ApplyT(func(arg any) corev1.ContainerArgs {
return arg.(corev1.ContainerArgs)
}).(corev1.ContainerOutput))
Expand All @@ -239,7 +244,7 @@ func addCloudsqlProxySidecarPreProcessor(ctx *sdk.Context, params appendParams)
})
}

func createCloudsqlProxy(ctx *sdk.Context, params appendParams) (*CloudSQLProxy, error) {
func createCloudsqlProxy(ctx *sdk.Context, params appendParams, namespaceOutput sdk.StringInput) (*CloudSQLProxy, error) {
// Fix for custom stacks: ensure input.StackParams.ParentEnv is set correctly for proper resource naming
if params.provisionParams.ParentStack != nil && params.provisionParams.ParentStack.ParentEnv != "" &&
params.provisionParams.ParentStack.ParentEnv != params.input.StackParams.Environment {
Expand All @@ -251,16 +256,6 @@ func createCloudsqlProxy(ctx *sdk.Context, params appendParams) (*CloudSQLProxy,
// This ensures service accounts are unique per environment (e.g., telegram-bot--on-sidecarcsql--production vs telegram-bot--on-sidecarcsql--staging)
baseProxyName := fmt.Sprintf("%s-%s-sidecarcsql", params.stack.Name, params.postgresName)
cloudsqlProxyName := kubernetes.SanitizeK8sName(params.input.ToResName(baseProxyName))
// The CloudSQL proxy emits a Kubernetes Secret (proxy credentials) that must live in
// the same namespace as the consuming pod for it to be mountable. For custom stacks
// (parentEnv != stackEnv) the pod namespace is `<stackName>-<stackEnv>` per
// kubernetes.GenerateNamespaceName, so derive that here.
parentEnv := ""
if params.provisionParams.ParentStack != nil {
parentEnv = params.provisionParams.ParentStack.ParentEnv
}
derivedNamespace := kubernetes.GenerateNamespaceName(params.input.StackParams.StackName, params.input.StackParams.Environment, parentEnv)
sanitizedNamespace := kubernetes.SanitizeK8sName(derivedNamespace)
cloudsqlProxy, err := NewCloudsqlProxy(ctx, CloudSQLProxyArgs{
Name: cloudsqlProxyName,
DBInstance: PostgresDBInstanceArgs{
Expand All @@ -270,7 +265,7 @@ func createCloudsqlProxy(ctx *sdk.Context, params appendParams) (*CloudSQLProxy,
},
GcpProvider: params.gcpProvider,
KubeProvider: params.kubeProvider,
Metadata: cloudsqlProxyMeta(sanitizedNamespace, cloudsqlProxyName, params),
Metadata: cloudsqlProxyMetaFromOutput(namespaceOutput, cloudsqlProxyName, params),
})
if err != nil {
return nil, err
Expand Down Expand Up @@ -354,27 +349,25 @@ func createUserForDatabase(ctx *sdk.Context, userName, dbName string, params app
// Sanitize names to comply with Kubernetes RFC 1123 requirements (no underscores)
cloudsqlProxyName := kubernetes.SanitizeK8sName(util.TrimStringMiddle(fmt.Sprintf("%s-%s-initcsql", userName, params.postgresName), 60, "-"))
// Init job + ad-hoc CloudSQL proxy must live in the same namespace as the consuming
// pod so the proxy's credential Secret is mountable. For custom stacks the pod is
// in `<stackName>-<stackEnv>` per kubernetes.GenerateNamespaceName.
parentEnv := ""
if params.provisionParams.ParentStack != nil {
parentEnv = params.provisionParams.ParentStack.ParentEnv
}
namespace := kubernetes.SanitizeK8sName(kubernetes.GenerateNamespaceName(params.input.StackParams.StackName, params.input.StackParams.Environment, parentEnv))
// pod so the proxy's credential Secret is mountable. Use the live Namespace.Metadata.Name()
// Output from the just-deployed SimpleContainer (instead of recomputing via
// GenerateNamespaceName), so we follow #255's IgnoreChanges("metadata.name") on the
// Namespace: migrated stacks stay parent-shared, fresh stacks get the isolated name.
namespaceOutput := sc.Namespace
cloudsqlProxy, err := NewCloudsqlProxy(ctx, CloudSQLProxyArgs{
Name: cloudsqlProxyName,
DBInstance: dbInstanceArgs,
GcpProvider: params.gcpProvider,
KubeProvider: params.kubeProvider,
TimeoutSec: MaxInitSQLTimeSec,
Metadata: cloudsqlProxyMeta(namespace, cloudsqlProxyName, params),
Metadata: cloudsqlProxyMetaFromOutput(namespaceOutput, cloudsqlProxyName, params),
}, sdk.DependsOn([]sdk.Resource{sc}))
if err != nil {
return errors.Wrapf(err, "failed to init cloudsql proxy")
}

_, err = NewInitDbUserJob(ctx, userName, InitDbUserJobArgs{
Namespace: namespace,
Namespace: namespaceOutput,
User: CloudsqlDbUser{
Database: dbName,
Username: userName,
Expand All @@ -395,9 +388,9 @@ func createUserForDatabase(ctx *sdk.Context, userName, dbName string, params app
return password, nil
}

func cloudsqlProxyMeta(namespace string, cloudsqlProxyName string, params appendParams) *v1.ObjectMetaArgs {
func cloudsqlProxyMetaFromOutput(namespace sdk.StringInput, cloudsqlProxyName string, params appendParams) *v1.ObjectMetaArgs {
return &v1.ObjectMetaArgs{
Namespace: sdk.String(namespace),
Namespace: namespace,
Name: sdk.String(cloudsqlProxyName),
Labels: sdk.StringMap{
kubernetes.LabelAppType: sdk.String(kubernetes.AppTypeSimpleContainer),
Expand Down
17 changes: 11 additions & 6 deletions pkg/clouds/pulumi/gcp/init_pg_user_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,13 @@ type InitDbUserJobArgs struct {
CloudSQLProxy *CloudSQLProxy
KubeProvider *sdkK8s.Provider
DBInstanceType CloudsqlInstanceType
Namespace string
Opts []sdk.ResourceOption
// Namespace is the live Namespace name Output, threaded from
// SimpleContainerArgs.NamespaceNameOutput / SimpleContainer.Namespace via
// compute_proc.go. Do not re-derive via kubernetes.GenerateNamespaceName —
// that drifts from the actual k8s name on migrated stacks where #255's
// IgnoreChanges("metadata.name") keeps the Namespace parent-shared.
Namespace sdk.StringInput
Opts []sdk.ResourceOption
}

type InitUserJob struct {
Expand All @@ -56,7 +61,7 @@ func NewInitDbUserJob(ctx *sdk.Context, stackName string, args InitDbUserJobArgs
// Secret creation
jobCredsSecret, err := corev1.NewSecret(ctx, jobCredsName, &corev1.SecretArgs{
Metadata: &v1.ObjectMetaArgs{
Namespace: sdk.String(args.Namespace),
Namespace: args.Namespace,
Name: sdk.String(jobCredsName),
},
StringData: sdk.StringMap{
Expand All @@ -76,14 +81,14 @@ func NewInitDbUserJob(ctx *sdk.Context, stackName string, args InitDbUserJobArgs
if args.DBInstanceType == MySQL {
initScript = `
set -e;
apk add --no-cache mysql-client;
apk add --no-cache mysql-client;
sleep 20;
# MySQL-specific logic here
`
} else {
initScript = fmt.Sprintf(`
set -e;
apk add --no-cache postgresql-client;
apk add --no-cache postgresql-client;
sleep 20;
psql -h localhost -U postgres -d %s -c 'GRANT pg_read_all_data TO "%s";';
psql -h localhost -U postgres -d %s -c 'GRANT pg_write_all_data TO "%s";';
Expand Down Expand Up @@ -121,7 +126,7 @@ psql -h localhost -U postgres -d %s -c 'GRANT pg_write_all_data TO "%s";';
job, err := batchv1.NewJob(ctx, jobName, &batchv1.JobArgs{
Metadata: &v1.ObjectMetaArgs{
Name: sdk.String(jobName),
Namespace: sdk.String(namespace),
Namespace: namespace,
Annotations: sdk.StringMap{
"pulumi.com/patchForce": sdk.String("true"),
},
Expand Down
18 changes: 8 additions & 10 deletions pkg/clouds/pulumi/kubernetes/compute_proc_mongodb.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,18 +130,16 @@ func createMongodbUserForDatabase(ctx *sdk.Context, userName, dbName string, par
ctx.Export(passwordName, password.Result)

// The init job must run in the same namespace as the consuming pod so it can be
// observed and cleaned up alongside the workload. For custom stacks (parentEnv != stackEnv)
// the pod lives in `<stackName>-<stackEnv>` per GenerateNamespaceName, so derive the same
// namespace here. Standard stacks keep the existing `<stackName>` namespace.
parentEnv := ""
if params.provisionParams.ParentStack != nil {
parentEnv = params.provisionParams.ParentStack.ParentEnv
}
namespace := GenerateNamespaceName(params.input.StackParams.StackName, params.input.StackParams.Environment, parentEnv)

// observed and cleaned up alongside the workload. Use the live Namespace.Metadata.Name()
// Output threaded through SimpleContainerArgs.NamespaceNameOutput — see the matching
// rationale in compute_proc_postgres.go.
params.collector.AddPreProcessor(&SimpleContainerArgs{}, func(c any) error {
kubeArgs, ok := c.(*SimpleContainerArgs)
if !ok {
return errors.Errorf("arg is not *SimpleContainerArgs")
}
_, err = NewMongodbInitDbUserJob(ctx, userName, InitDbUserJobArgs{
Namespace: namespace,
Namespace: kubeArgs.NamespaceNameOutput,
User: DatabaseUser{
Database: dbName,
Username: userName,
Expand Down
20 changes: 10 additions & 10 deletions pkg/clouds/pulumi/kubernetes/compute_proc_postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,18 +211,18 @@ func createPostgresUserForDatabase(ctx *sdk.Context, userName, dbName string, pa
}

// The init job must run in the same namespace as the consuming pod so it can be
// observed and cleaned up alongside the workload. For custom stacks (parentEnv != stackEnv)
// the pod lives in `<stackName>-<stackEnv>` per GenerateNamespaceName, so derive the same
// namespace here. Standard stacks keep the existing `<stackName>` namespace.
parentEnv := ""
if params.provisionParams.ParentStack != nil {
parentEnv = params.provisionParams.ParentStack.ParentEnv
}
namespace := GenerateNamespaceName(params.input.StackParams.StackName, params.input.StackParams.Environment, parentEnv)

// observed and cleaned up alongside the workload. Use the live Namespace.Metadata.Name()
// Output threaded through SimpleContainerArgs.NamespaceNameOutput (set by NewSimpleContainer
// after the Namespace is created, before this PreProcessor fires) — recomputing via
// GenerateNamespaceName would drift from the actual k8s name on migrated stacks where #255's
// IgnoreChanges("metadata.name") keeps the Namespace parent-shared.
params.collector.AddPreProcessor(&SimpleContainerArgs{}, func(c any) error {
kubeArgs, ok := c.(*SimpleContainerArgs)
if !ok {
return errors.Errorf("arg is not *SimpleContainerArgs")
}
_, err = NewPostgresInitDbUserJob(ctx, userName, InitDbUserJobArgs{
Namespace: namespace,
Namespace: kubeArgs.NamespaceNameOutput,
User: DatabaseUser{
Database: dbName,
Username: userName,
Expand Down
39 changes: 34 additions & 5 deletions pkg/clouds/pulumi/kubernetes/init_mongo_user_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func NewMongodbInitDbUserJob(ctx *sdk.Context, stackName string, args InitDbUser
// Secret creation
jobCredsSecret, err := corev1.NewSecret(ctx, jobCredsName, &corev1.SecretArgs{
Metadata: &v1.ObjectMetaArgs{
Namespace: sdk.String(args.Namespace),
Namespace: args.Namespace,
Name: sdk.String(jobCredsName),
},
StringData: sdk.StringMap{
Expand All @@ -41,10 +41,40 @@ func NewMongodbInitDbUserJob(ctx *sdk.Context, stackName string, args InitDbUser
if err != nil {
return nil, err
}
// Idempotent user provisioning: db.createUser errors with code 51003 (DuplicateKey)
// if the user already exists, which would break any re-run of this Job — including
// the Replace that happens when a consumer follows #255's documented opt-in
// namespace migration (`pulumi stack export | jq 'del(...Namespace urn...)' |
// pulumi stack import`). Use createUser-or-updateUser semantics so the Job
// succeeds on both the first and any subsequent run. Matches the idempotency
// guarantees the postgres init scripts already provide via `IF NOT EXISTS`
// guards (pkg/clouds/pulumi/db/constants.go) and `GRANT` idempotency
// (pkg/clouds/pulumi/gcp/init_pg_user_job.go).
//
// Credentials read from process.env inside the mongosh eval rather than shell
// interpolation. Pulling them via env (a) bypasses shell quoting entirely so
// passwords containing spaces/quotes/$ don't bash-word-split or break JS
// parsing, (b) keeps the secret out of `ps`/strace visibility on the
// command line. The connection URI itself still has to be shell-interpolated
// because mongosh consumes it as its first positional argument, but the user
// password is the higher-risk value and is now end-to-end env-bound.
createUserScript := `
set -e;
mongosh "mongodb://${ROOT_USER}:${ROOT_PASSWORD}@${HOST}/${DB_NAME}?authSource=${ROOT_DATABASE}&readPreference=primary&replicaSet=${REPLICA_SET}" \
--eval "db.createUser({user:'${DB_USER}',pwd:'${DB_PASSWORD}',roles:[{db: '${DB_NAME}', role: 'dbAdmin'}, {db: '${DB_NAME}', role: 'readWrite'}, {db: 'local', role: 'read'}]})"
mongosh "mongodb://${ROOT_USER}:${ROOT_PASSWORD}@${HOST}/${DB_NAME}?authSource=${ROOT_DATABASE}&readPreference=primary&replicaSet=${REPLICA_SET}" --eval '
var dbName = process.env.DB_NAME;
var dbUser = process.env.DB_USER;
var dbPwd = process.env.DB_PASSWORD;
var roles = [
{db: dbName, role: "dbAdmin"},
{db: dbName, role: "readWrite"},
{db: "local", role: "read"}
];
if (db.getUser(dbUser) === null) {
db.createUser({user: dbUser, pwd: dbPwd, roles: roles});
} else {
db.updateUser(dbUser, {pwd: dbPwd, roles: roles});
}
'
`
// Job Container creation
jobContainer := corev1.ContainerArgs{
Expand All @@ -65,13 +95,12 @@ mongosh "mongodb://${ROOT_USER}:${ROOT_PASSWORD}@${HOST}/${DB_NAME}?authSource=$
}

kubeProvider := args.KubeProvider
namespace := args.Namespace

// Job creation
job, err := batchv1.NewJob(ctx, jobName, &batchv1.JobArgs{
Metadata: &v1.ObjectMetaArgs{
Name: sdk.String(jobName),
Namespace: sdk.String(namespace),
Namespace: args.Namespace,
Annotations: sdk.StringMap{
"pulumi.com/patchForce": sdk.String("true"),
},
Expand Down
19 changes: 11 additions & 8 deletions pkg/clouds/pulumi/kubernetes/init_pg_user_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,15 @@ type InitDbUserJobArgs struct {
RootUser string
RootPassword string
KubeProvider sdk.ProviderResource
Namespace string
Host string
Port string
InitSQL string
Opts []sdk.ResourceOption
// Namespace is the live Namespace name Output, threaded through from
// SimpleContainerArgs.NamespaceNameOutput via compute_proc_postgres.go and
// compute_proc_mongodb.go. Do not re-derive via GenerateNamespaceName —
// see the long rationale on the matching field in gcp.InitDbUserJobArgs.
Namespace sdk.StringInput
Host string
Port string
InitSQL string
Opts []sdk.ResourceOption
}

type InitUserJob struct {
Expand All @@ -45,7 +49,7 @@ func NewPostgresInitDbUserJob(ctx *sdk.Context, stackName string, args InitDbUse
// Secret creation
jobCredsSecret, err := corev1.NewSecret(ctx, jobCredsName, &corev1.SecretArgs{
Metadata: &v1.ObjectMetaArgs{
Namespace: sdk.String(args.Namespace),
Namespace: args.Namespace,
Name: sdk.String(jobCredsName),
},
StringData: sdk.StringMap{
Expand Down Expand Up @@ -82,13 +86,12 @@ func NewPostgresInitDbUserJob(ctx *sdk.Context, stackName string, args InitDbUse
}

kubeProvider := args.KubeProvider
namespace := args.Namespace

// Job creation
job, err := batchv1.NewJob(ctx, jobName, &batchv1.JobArgs{
Metadata: &v1.ObjectMetaArgs{
Name: sdk.String(jobName),
Namespace: sdk.String(namespace),
Namespace: args.Namespace,
Annotations: sdk.StringMap{
"pulumi.com/patchForce": sdk.String("true"),
},
Expand Down
Loading
Loading