Skip to content

Commit

Permalink
feat: added default postgres provisioner
Browse files Browse the repository at this point in the history
Signed-off-by: Ben Meier <[email protected]>
  • Loading branch information
astromechza committed Mar 12, 2024
1 parent c5f687d commit 539b669
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 15 deletions.
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/Masterminds/sprig/v3 v3.2.3
github.com/compose-spec/compose-go/v2 v2.0.0-rc.8
github.com/imdario/mergo v0.3.13
github.com/mitchellh/mapstructure v1.5.0
github.com/score-spec/score-go v1.2.0
github.com/spf13/cobra v1.6.0
github.com/spf13/pflag v1.0.5
Expand All @@ -29,7 +30,6 @@ require (
github.com/kr/pretty v0.1.0 // indirect
github.com/mattn/go-shellwords v1.0.12 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
Expand All @@ -40,6 +40,9 @@ require (
github.com/tidwall/gjson v1.14.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
golang.org/x/crypto v0.3.0 // indirect
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect
golang.org/x/sync v0.3.0 // indirect
Expand Down
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
Expand All @@ -77,6 +78,12 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
Expand Down
74 changes: 74 additions & 0 deletions internal/command/default.provisioners.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
labels:
dev.score.compose.res.uid: {{ .Uid }}
image: redis:7
restart: always
entrypoint: ["redis-server"]
command: ["/usr/local/etc/redis/redis.conf"]
volumes:
Expand All @@ -74,3 +75,76 @@
target: /data
volume:
nocopy: true
# The default postgres provisioner adds a postgres instance and then ensures that the required databases are created on
# startup.
- uri: template://default-provisioners/postgres
# By default, match all redis types regardless of class and id. If you want to override this, create another
# provisioner definition with a higher priority.
type: postgres
# Init template has the random service name and password if needed later
init: |
randomServiceName: pg-{{ randAlphaNum 6 }}
randomDatabase: db-{{ randAlpha 8 }}
randomUsername: user-{{ randAlpha 8 }}
randomPassword: {{ randAlphaNum 16 | quote }}
sk: default-provisioners-postgres-instance
state: |
database: {{ dig "database" .Init.randomDatabase .State | quote }}
username: {{ dig "username" .Init.randomUsername .State | quote }}
password: {{ dig "password" .Init.randomPassword .State | quote }}
shared: |
{{ .Init.sk }}:
instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }}
instancePassword: {{ dig .Init.sk "instancePassword" .Init.randomPassword .Shared | quote }}
outputs: |
host: {{ dig .Init.sk "instanceServiceName" "" .Shared }}
port: 5432
name: {{ .State.database }}
database: {{ .State.database }}
username: {{ .State.username }}
password: {{ .State.password }}
directories: |
{{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts: false
files: |
{{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts/{{ .State.database }}.sql: |
SELECT 'CREATE DATABASE "{{ .State.database }}"' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '{{ .State.database }}')\gexec
SELECT $$CREATE USER "{{ .State.username }}" WITH PASSWORD '{{ .State.password }}'$$ WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '{{ .State.username }}')\gexec
GRANT ALL PRIVILEGES ON DATABASE "{{ .State.database }}" TO "{{ .State.username }}";
volumes: |
{{ dig .Init.sk "instanceServiceName" "" .Shared }}-data:
driver: local
services: |
{{ dig .Init.sk "instanceServiceName" "" .Shared }}:
image: postgres:16-alpine
restart: always
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: {{ dig .Init.sk "instancePassword" "" .Shared | quote }}
volumes:
- type: volume
source: {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data
target: /var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 2s
timeout: 2s
retries: 10
{{ dig .Init.sk "instanceServiceName" "" .Shared }}-init:
image: postgres:16-alpine
entrypoint: ["/bin/sh"]
environment:
POSTGRES_PASSWORD: {{ dig .Init.sk "instancePassword" "" .Shared | quote }}
command:
- "-c"
- |
cd /db-scripts
ls db-*.sql | xargs cat | psql "postgresql://postgres:$${POSTGRES_PASSWORD}@{{ dig .Init.sk "instanceServiceName" "" .Shared }}:5432/postgres"
depends_on:
{{ dig .Init.sk "instanceServiceName" "" .Shared }}:
condition: service_healthy
restart: true
volumes:
- type: bind
source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts
target: /db-scripts
10 changes: 10 additions & 0 deletions internal/provisioners/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ type Provisioner interface {
// ApplyToStateAndProject takes the outputs of a provisioning request and applies to the state, file tree, and docker
// compose project.
func (po *ProvisionOutput) ApplyToStateAndProject(state *project.State, resUid project.ResourceUid, project *compose.Project) (*project.State, error) {
slog.Debug(
fmt.Sprintf("Provisioned resource '%s'", resUid),
"outputs", po.ResourceOutputs,
"#directories", len(po.RelativeDirectories),
"#files", len(po.RelativeFileContents),
"#volumes", len(po.ComposeVolumes),
"#networks", len(po.ComposeNetworks),
"#services", len(po.ComposeServices),
)

out := *state
out.Resources = maps.Clone(state.Resources)

Expand Down
37 changes: 23 additions & 14 deletions internal/provisioners/templateprov/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
"text/template"

"github.com/Masterminds/sprig/v3"
"github.com/compose-spec/compose-go/v2/loader"
compose "github.com/compose-spec/compose-go/v2/types"
"github.com/mitchellh/mapstructure"
"gopkg.in/yaml.v3"

"github.com/score-spec/score-compose/internal/project"
Expand Down Expand Up @@ -89,7 +91,7 @@ func (p *Provisioner) Match(resUid project.ResourceUid) bool {
return true
}

func renderTemplateAndDecode(raw string, data interface{}, out interface{}) error {
func renderTemplateAndDecode(raw string, data interface{}, out interface{}, withComposeExtensions bool) error {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil
Expand All @@ -103,15 +105,22 @@ func renderTemplateAndDecode(raw string, data interface{}, out interface{}) erro
return fmt.Errorf("failed to execute template: %w", err)
}
buffContents := buff.String()
if strings.TrimSpace(buffContents) == "" {
if strings.TrimSpace(buff.String()) == "" {
return nil
}
dec := yaml.NewDecoder(strings.NewReader(buffContents))
dec.KnownFields(true)
if err := dec.Decode(out); err != nil {
var intermediate map[string]interface{}
if err := yaml.Unmarshal([]byte(buffContents), &intermediate); err != nil {
slog.Debug(fmt.Sprintf("template output was '%s' from template '%s'", buffContents, raw))
return fmt.Errorf("failed to decode output: %w", err)
}
if withComposeExtensions {
err = loader.Transform(intermediate, &out)
} else {
err = mapstructure.Decode(intermediate, &out)
}
if err != nil {
return fmt.Errorf("failed to decode output: %w", err)
}
return nil
}

Expand Down Expand Up @@ -148,50 +157,50 @@ func (p *Provisioner) Provision(ctx context.Context, input *provisioners.Input)
}

init := make(map[string]interface{})
if err := renderTemplateAndDecode(p.InitTemplate, &data, &init); err != nil {
if err := renderTemplateAndDecode(p.InitTemplate, &data, &init, false); err != nil {
return nil, fmt.Errorf("init template failed: %w", err)
}
data.Init = init

out.ResourceState = make(map[string]interface{})
if err := renderTemplateAndDecode(p.StateTemplate, &data, &out.ResourceState); err != nil {
if err := renderTemplateAndDecode(p.StateTemplate, &data, &out.ResourceState, false); err != nil {
return nil, fmt.Errorf("state template failed: %w", err)
}
data.State = out.ResourceState

out.SharedState = make(map[string]interface{})
if err := renderTemplateAndDecode(p.SharedStateTemplate, &data, &out.SharedState); err != nil {
if err := renderTemplateAndDecode(p.SharedStateTemplate, &data, &out.SharedState, false); err != nil {
return nil, fmt.Errorf("shared template failed: %w", err)
}
data.Shared = util.PatchMap(data.Shared, out.SharedState)

out.ResourceOutputs = make(map[string]interface{})
if err := renderTemplateAndDecode(p.OutputsTemplate, &data, &out.ResourceOutputs); err != nil {
if err := renderTemplateAndDecode(p.OutputsTemplate, &data, &out.ResourceOutputs, false); err != nil {
return nil, fmt.Errorf("outputs template failed: %w", err)
}

out.RelativeDirectories = make(map[string]bool)
if err := renderTemplateAndDecode(p.RelativeDirectoriesTemplate, &data, &out.RelativeDirectories); err != nil {
if err := renderTemplateAndDecode(p.RelativeDirectoriesTemplate, &data, &out.RelativeDirectories, false); err != nil {
return nil, fmt.Errorf("directories template failed: %w", err)
}

out.RelativeFileContents = make(map[string]*string)
if err := renderTemplateAndDecode(p.RelativeFilesTemplate, &data, &out.RelativeFileContents); err != nil {
if err := renderTemplateAndDecode(p.RelativeFilesTemplate, &data, &out.RelativeFileContents, false); err != nil {
return nil, fmt.Errorf("files template failed: %w", err)
}

out.ComposeNetworks = make(map[string]compose.NetworkConfig)
if err := renderTemplateAndDecode(p.ComposeNetworksTemplate, &data, &out.ComposeNetworks); err != nil {
if err := renderTemplateAndDecode(p.ComposeNetworksTemplate, &data, &out.ComposeNetworks, true); err != nil {
return nil, fmt.Errorf("networks template failed: %w", err)
}

out.ComposeServices = make(map[string]compose.ServiceConfig)
if err := renderTemplateAndDecode(p.ComposeServicesTemplate, &data, &out.ComposeServices); err != nil {
if err := renderTemplateAndDecode(p.ComposeServicesTemplate, &data, &out.ComposeServices, true); err != nil {
return nil, fmt.Errorf("services template failed: %w", err)
}

out.ComposeVolumes = make(map[string]compose.VolumeConfig)
if err := renderTemplateAndDecode(p.ComposeVolumesTemplate, &data, &out.ComposeVolumes); err != nil {
if err := renderTemplateAndDecode(p.ComposeVolumesTemplate, &data, &out.ComposeVolumes, true); err != nil {
return nil, fmt.Errorf("volumes template failed: %w", err)
}

Expand Down

0 comments on commit 539b669

Please sign in to comment.