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
2 changes: 1 addition & 1 deletion agent.codefly.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
publisher: codefly.dev
kind: codefly:service
name: postgres
version: 0.0.116
version: 0.0.117
105 changes: 103 additions & 2 deletions bootstrap_template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ package main

import (
"bytes"
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"text/template"
"time"
)

func TestBootstrapImageAlwaysReconcilesRuntimeAccess(t *testing.T) {
Expand Down Expand Up @@ -38,8 +43,18 @@ func TestBootstrapImageAlwaysReconcilesRuntimeAccess(t *testing.T) {
) {
t.Fatal("bootstrap image does not wait for Postgres readiness")
}
if !strings.Contains(dockerfile, "/releases/download/v4.19.1/migrate.linux-amd64.tar.gz") {
t.Fatal("bootstrap image does not pin the supported migration runtime")
for _, required := range []string{
"ARG TARGETARCH",
`architecture="${TARGETARCH:-$(apk --print-arch)}"`,
"x86_64) architecture=amd64",
"aarch64) architecture=arm64",
`case "${architecture}" in`,
"amd64|arm64)",
"/releases/download/v4.19.1/migrate.linux-${architecture}.tar.gz",
} {
if !strings.Contains(dockerfile, required) {
t.Fatalf("bootstrap image is not target-architecture portable: missing %q", required)
}
}
hasMigration := strings.Contains(dockerfile, "/usr/local/bin/migrate -path")
if hasMigration != test.withMigrations {
Expand Down Expand Up @@ -70,6 +85,92 @@ func TestBootstrapImageAlwaysReconcilesRuntimeAccess(t *testing.T) {
}
}

func TestBootstrapImageBuildsWhenDockerOmitsTargetArchitecture(t *testing.T) {
if _, err := exec.LookPath("docker"); err != nil {
t.Fatal(err)
}
root := t.TempDir()
parameters := DockerTemplating{
MigrationConnectionKeyHolder: "{" + migrationConnectionEnvironmentKey + "}",
}
if err := os.WriteFile(
filepath.Join(root, "Dockerfile"),
[]byte(renderBuilderTemplate(t, "templates/builder/Dockerfile.tmpl", parameters)),
0o644,
); err != nil {
t.Fatal(err)
}
builderDir := filepath.Join(root, "builder")
if err := os.MkdirAll(builderDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(builderDir, "runtime-access.sql"), []byte("SELECT 1;\n"), 0o644); err != nil {
t.Fatal(err)
}
tag := fmt.Sprintf("service-postgres-bootstrap-targetarch-test:%d", time.Now().UnixNano())
t.Cleanup(func() {
_ = exec.Command("docker", "image", "rm", tag).Run()
})
command := exec.Command("docker", "build", "--build-arg", "TARGETARCH=", "--tag", tag, root)
if output, err := command.CombinedOutput(); err != nil {
t.Fatalf("legacy Docker build without TARGETARCH failed: %v\n%s", err, output)
}
}

func TestRuntimeAccessTemplateUsesDelegatedRolesAsExclusiveWriteAuthority(t *testing.T) {
parameters := DockerTemplating{
MigrationConnectionKeyHolder: "{" + migrationConnectionEnvironmentKey + "}",
ReadOnlyRole: "codefly_app_ro",
ReadWriteRole: "codefly_app_rw",
Schemas: []string{"public"},
ReadWriteRoles: []string{"app_tenant", "app_worker"},
}

accessSQL := renderBuilderTemplate(t, "templates/builder/runtime-access.sql.tmpl", parameters)
for _, forbidden := range []string{
"GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES",
"GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES",
"GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES",
"GRANT USAGE, SELECT, UPDATE ON SEQUENCES",
} {
if strings.Contains(accessSQL, forbidden) {
t.Fatalf("delegated read-write login retained direct authority %q", forbidden)
}
}
for _, required := range []string{
"REVOKE ALL PRIVILEGES ON ALL TABLES",
"REVOKE ALL PRIVILEGES ON ALL SEQUENCES",
"GRANT %I TO %I",
"app_tenant",
"app_worker",
} {
if !strings.Contains(accessSQL, required) {
t.Fatalf("delegated runtime access is missing %q", required)
}
}
}

func TestRuntimeAccessTemplatePreservesDirectWriterWithoutDelegatedRoles(t *testing.T) {
parameters := DockerTemplating{
MigrationConnectionKeyHolder: "{" + migrationConnectionEnvironmentKey + "}",
ReadOnlyRole: "codefly_app_ro",
ReadWriteRole: "codefly_app_rw",
Schemas: []string{"public"},
}

accessSQL := renderBuilderTemplate(t, "templates/builder/runtime-access.sql.tmpl", parameters)
for _, required := range []string{
"GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES",
"GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES",
"GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES",
"GRANT USAGE, SELECT, UPDATE ON SEQUENCES",
} {
if !strings.Contains(accessSQL, required) {
t.Fatalf("direct runtime access is missing %q", required)
}
}
}

func renderBuilderTemplate(t *testing.T, name string, parameters DockerTemplating) string {
t.Helper()
source, err := fs.ReadFile(builderFS, name)
Expand Down
82 changes: 76 additions & 6 deletions builder.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package main

import (
"bytes"
"context"
"crypto/sha256"
"embed"
"encoding/hex"
"fmt"
"io/fs"
"strings"
"text/template"

"github.com/codefly-dev/core/agents/communicate"
dockerhelpers "github.com/codefly-dev/core/agents/helpers/docker"
Expand All @@ -16,6 +22,7 @@ import (
"github.com/codefly-dev/core/agents/services/upgrade"
builderv0 "github.com/codefly-dev/core/generated/go/codefly/services/builder/v0"
"github.com/codefly-dev/core/shared"
"gopkg.in/yaml.v3"
)

type Builder struct {
Expand Down Expand Up @@ -180,7 +187,7 @@ func (s *Builder) Deploy(ctx context.Context, req *builderv0.DeploymentRequest)
ManagedImage: s.dockerImage().FullName(),
DatabaseName: s.DatabaseName,
}
var promotableConfiguration *v0.Configuration
var restrictedConfiguration *v0.Configuration
response, err := s.Builder.DeployKustomize(ctx, req, services.KustomizeDeployment{
EnvironmentVariables: s.EnvironmentVariables,
Templates: deploymentFS,
Expand All @@ -190,23 +197,86 @@ func (s *Builder) Deploy(ctx context.Context, req *builderv0.DeploymentRequest)
if prepareErr != nil {
return prepareErr
}
bootstrapJobName, nameErr := s.immutableBootstrapJobName(deployment, parameters)
if nameErr != nil {
return nameErr
}
parameters.BootstrapJobName = bootstrapJobName
s.Wool.Debug("exporting configuration", wool.Field("conf", resources.MakeConfigurationSummary(configuration)))
if deployment.Profile == builderv0.KubernetesOutputProfile_KUBERNETES_OUTPUT_PROFILE_PROMOTABLE_GITOPS_V1 {
promotableConfiguration = configuration
if services.IsRestrictedOutputProfile(deployment.Profile) {
restrictedConfiguration = configuration
return nil
}
return deployment.ExportConfiguration(ctx, configuration)
},
})
if err != nil ||
response.GetState().GetState() != builderv0.DeploymentStatus_SUCCESS ||
promotableConfiguration == nil {
restrictedConfiguration == nil {
return response, err
}
response.Configuration = promotableConfiguration
response.Configuration = restrictedConfiguration
return response, nil
}

const bootstrapJobTemplatePath = "templates/deployment/kustomize/base/job.yaml.tmpl"

func (s *Builder) immutableBootstrapJobName(
deployment *services.KustomizeDeploymentContext,
parameters *DeploymentTemplateParameters,
) (string, error) {
service := shared.ToDNSCase(s.Identity.Name)
if service == "" {
return "", fmt.Errorf("bootstrap service name is required")
}

source, err := fs.ReadFile(deploymentFS, bootstrapJobTemplatePath)
if err != nil {
return "", fmt.Errorf("read bootstrap Job template: %w", err)
}
jobTemplate, err := template.New(bootstrapJobTemplatePath).Parse(string(source))
if err != nil {
return "", fmt.Errorf("parse bootstrap Job template: %w", err)
}
renderContext := &services.DeploymentWrapper{
DeploymentBase: &services.DeploymentBase{
Information: s.Information,
Namespace: deployment.Kubernetes.GetNamespace(),
Image: s.DockerImage(deployment.Kubernetes.GetBuildContext()),
Profile: deployment.Profile,
Restricted: services.IsRestrictedOutputProfile(deployment.Profile),
},
Deployment: services.DeploymentParameters{Parameters: parameters},
}
var rendered bytes.Buffer
if err = jobTemplate.Execute(&rendered, renderContext); err != nil {
return "", fmt.Errorf("render bootstrap Job template: %w", err)
}
var job struct {
Spec struct {
Template yaml.Node `yaml:"template"`
} `yaml:"spec"`
}
if err = yaml.Unmarshal(rendered.Bytes(), &job); err != nil {
return "", fmt.Errorf("parse rendered bootstrap Job: %w", err)
}
if job.Spec.Template.Kind == 0 {
return "", fmt.Errorf("rendered bootstrap Job is missing spec.template")
}
podTemplate, err := yaml.Marshal(&job.Spec.Template)
if err != nil {
return "", fmt.Errorf("encode bootstrap Job pod template: %w", err)
}
contentDigest := sha256.Sum256(podTemplate)

const suffixLength = 12
const maxServiceLength = 63 - 1 - suffixLength
if len(service) > maxServiceLength {
service = strings.TrimRight(service[:maxServiceLength], "-")
}
return service + "-" + hex.EncodeToString(contentDigest[:])[:suffixLength], nil
}

func (s *Builder) prepareDeployment(
ctx context.Context,
deployment *services.KustomizeDeploymentContext,
Expand All @@ -222,7 +292,7 @@ func (s *Builder) prepareDeployment(
if err != nil {
return nil, err
}
if deployment.Profile == builderv0.KubernetesOutputProfile_KUBERNETES_OUTPUT_PROFILE_PROMOTABLE_GITOPS_V1 {
if services.IsRestrictedOutputProfile(deployment.Profile) {
workloadReferences, referencesErr := s.selectPromotableSecretReferences(
deployment.Kubernetes.GetSecretReferences(),
)
Expand Down
29 changes: 29 additions & 0 deletions capability_probe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,21 @@ func (p *postgresCapabilityProbe) AppendTenantFixture(ctx context.Context, relat
return err
}

func (p *postgresCapabilityProbe) AppendFixtureAsRole(ctx context.Context, role, relation, id string) error {
tx, err := p.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
if _, err := tx.ExecContext(ctx, `SET LOCAL ROLE `+pq.QuoteIdentifier(role)); err != nil {
return err
}
if _, err := tx.ExecContext(ctx, `INSERT INTO `+pq.QuoteIdentifier(relation)+` (id) VALUES ($1)`, id); err != nil {
return err
}
return tx.Commit()
}

func (p *postgresCapabilityProbe) HasFixture(ctx context.Context, relation, id string) (bool, error) {
var exists bool
err := p.db.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM `+pq.QuoteIdentifier(relation)+` WHERE id = $1)`, id).Scan(&exists)
Expand Down Expand Up @@ -91,6 +106,20 @@ func (p *postgresCapabilityProbe) InstallTenantFixture(ctx context.Context, rela
return nil
}

func (p *postgresCapabilityProbe) InstallDelegatedWriteRole(ctx context.Context, role, relation string) error {
quotedRole := pq.QuoteIdentifier(role)
quotedRelation := pq.QuoteIdentifier(relation)
for _, statement := range []string{
`CREATE ROLE ` + quotedRole + ` NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS`,
`GRANT SELECT, INSERT, UPDATE, DELETE ON ` + quotedRelation + ` TO ` + quotedRole,
} {
if _, err := p.db.ExecContext(ctx, statement); err != nil {
return err
}
}
return nil
}

type scopedFixtureRepository struct {
factory *scoped.Factory
relation string
Expand Down
Loading
Loading