diff --git a/api/v1alpha1/atlasschema_types.go b/api/v1alpha1/atlasschema_types.go index 9ae07a5..86a2598 100644 --- a/api/v1alpha1/atlasschema_types.go +++ b/api/v1alpha1/atlasschema_types.go @@ -267,7 +267,7 @@ func (s Schema) DesiredState(ctx context.Context, r client.Reader, ns string) (* } return u, nil, err } - return nil, nil, fmt.Errorf("no desired state specified") + return nil, nil, nil } // AsBlock returns the HCL block representation of the diff. diff --git a/internal/controller/atlasschema_controller.go b/internal/controller/atlasschema_controller.go index b839243..3b1c431 100644 --- a/internal/controller/atlasschema_controller.go +++ b/internal/controller/atlasschema_controller.go @@ -230,7 +230,7 @@ func (r *AtlasSchemaReconciler) Reconcile(ctx context.Context, req ctrl.Request) r.recordErrEvent(res, err) return result(err) } - switch desiredURL := data.Desired.String(); { + switch desiredURL := data.targetURL(); { // The resource is connected to Atlas Cloud. case whoami != nil: err = editAtlasHCL(func(m *managedData) { @@ -257,7 +257,7 @@ func (r *AtlasSchemaReconciler) Reconcile(ctx context.Context, req ctrl.Request) // This is to ensure that the schema is in sync with the Atlas Cloud. // And the schema is available for the Atlas CLI (on local machine) // to modify or approve the changes. - if data.Desired.Scheme == dbv1alpha1.SchemaTypeFile { + if data.Desired != nil && data.Desired.Scheme == dbv1alpha1.SchemaTypeFile { tag, err := cli.SchemaInspect(ctx, &atlasexec.SchemaInspectParams{ Env: data.EnvName, URL: desiredURL, @@ -794,9 +794,6 @@ func (d *managedData) render(w io.Writer) error { if d.EnvName == "" { return errors.New("env name is not set") } - if d.Desired == nil { - return errors.New("the desired state is not set") - } env := searchBlock(f.Body(), hclwrite.NewBlock("env", []string{d.EnvName})) if env == nil { return fmt.Errorf("env block %q is not found", d.EnvName) @@ -808,6 +805,10 @@ func (d *managedData) render(w io.Writer) error { if b.GetAttribute("dev") == nil { return errors.New("dev url is not set") } + schema := searchBlock(b, hclwrite.NewBlock("schema", nil)) + if schema == nil && b.GetAttribute("src") == nil { + return errors.New("the desired state is not set") + } if _, err := f.WriteTo(w); err != nil { return err } diff --git a/internal/controller/atlasschema_controller_test.go b/internal/controller/atlasschema_controller_test.go index b3402d8..3ec64f9 100644 --- a/internal/controller/atlasschema_controller_test.go +++ b/internal/controller/atlasschema_controller_test.go @@ -134,7 +134,7 @@ func TestReconcile_Reconcile(t *testing.T) { }, }) // Third reconcile, return error for missing schema - assert(ctrl.Result{RequeueAfter: 5000000000}, false, "ReadSchema", "no desired state specified") + assert(ctrl.Result{}, false, "CreatingWorkingDir", "the desired state is not set") // Add schema, h.patch(t, &dbv1alpha1.AtlasSchema{ ObjectMeta: meta, @@ -160,7 +160,7 @@ func TestReconcile_Reconcile(t *testing.T) { // Check the events generated by the controller require.Equal(t, []string{ "Warning TransientErr no target database defined", - "Warning TransientErr no desired state specified", + "Warning Error the desired state is not set", "Normal Applied Applied schema", "Normal Applied Applied schema", }, h.events()) diff --git a/internal/controller/lint.go b/internal/controller/lint.go index bf8acb5..42a6158 100644 --- a/internal/controller/lint.go +++ b/internal/controller/lint.go @@ -50,7 +50,7 @@ func (r *AtlasSchemaReconciler) lint(ctx context.Context, wd *atlasexec.WorkingD plans, err := cli.SchemaApplySlice(ctx, &atlasexec.SchemaApplyParams{ Env: data.EnvName, Vars: vars, - To: data.Desired.String(), + To: data.targetURL(), DryRun: true, // Dry run to get pending changes. }) if err != nil { diff --git a/test/e2e/testscript/schema-composite.txtar b/test/e2e/testscript/schema-composite.txtar new file mode 100644 index 0000000..8f6189d --- /dev/null +++ b/test/e2e/testscript/schema-composite.txtar @@ -0,0 +1,193 @@ +env SCHEMA_DB_URL=postgres://root:pass@postgres.${NAMESPACE}:5432/postgres?sslmode=disable +env SCHEMA_DB_DEV_URL=postgres://root:pass@postgres.${NAMESPACE}:5433/postgres?sslmode=disable +kubectl apply -f database.yaml +kubectl create secret generic schema-db-creds --from-literal=url=${SCHEMA_DB_URL} +kubectl create configmap schema-db-dev-creds --from-literal=url=${SCHEMA_DB_DEV_URL} +# Create the secret to store ATLAS_TOKEN +kubectl create secret generic atlas-token --from-literal=ATLAS_TOKEN=${ATLAS_TOKEN} + +# Wait for the first pod created +kubectl-wait-available deploy/postgres +# Wait for the DB ready before creating the schema +kubectl-wait-ready -l app=postgres pods + +# Create the schema +kubectl apply -f schema.yaml +kubectl wait --for=jsonpath='{.status.conditions[*].reason}'=Applied --timeout=500s AtlasSchemas/sample + +atlas schema inspect -u ${SCHEMA_DB_URL} +cmp stdout schema.hcl + +-- schema.yaml -- +apiVersion: db.atlasgo.io/v1alpha1 +kind: AtlasSchema +metadata: + name: sample +spec: + envName: "test" + cloud: + repo: atlas-operator + tokenFrom: + secretKeyRef: + name: atlas-token + key: ATLAS_TOKEN + vars: + - key: "db_url" + valueFrom: + secretKeyRef: + name: schema-db-creds + key: url + - key: "dev_db_url" + valueFrom: + configMapKeyRef: + name: schema-db-dev-creds + key: url + config: | + variable "db_url" { + type = string + } + variable "dev_db_url" { + type = string + } + data "external_schema" "users" { + program = ["echo", "CREATE TABLE users (id int not null, name varchar(255) not null, email varchar(255) not null, short_bio varchar(255) not null, PRIMARY KEY (id));"] + } + data "external_schema" "posts" { + program = ["echo", "CREATE TABLE posts (id int not null, title varchar(255) not null, body text not null, PRIMARY KEY (id));"] + } + data "composite_schema" "app" { + schema "public" { + url = data.external_schema.users.url + } + schema "public" { + url = data.external_schema.posts.url + } + } + env "test" { + schema { + src = data.composite_schema.app.url + } + url = var.db_url + dev = var.dev_db_url + } +-- schema.hcl -- +table "posts" { + schema = schema.public + column "id" { + null = false + type = integer + } + column "title" { + null = false + type = character_varying(255) + } + column "body" { + null = false + type = text + } + primary_key { + columns = [column.id] + } +} +table "users" { + schema = schema.public + column "id" { + null = false + type = integer + } + column "name" { + null = false + type = character_varying(255) + } + column "email" { + null = false + type = character_varying(255) + } + column "short_bio" { + null = false + type = character_varying(255) + } + primary_key { + columns = [column.id] + } +} +schema "public" { + comment = "standard public schema" +} +-- database.yaml -- +apiVersion: v1 +kind: Service +metadata: + name: postgres +spec: + selector: + app: postgres + ports: + - name: postgres + port: 5432 + targetPort: postgres + - name: postgres-dev + port: 5433 + targetPort: postgres-dev + type: ClusterIP +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres +spec: + selector: + matchLabels: + app: postgres + replicas: 1 + template: + metadata: + labels: + app: postgres + spec: + securityContext: + runAsNonRoot: true + runAsUser: 999 + containers: + - name: postgres + image: postgres:15.4 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - all + env: + - name: POSTGRES_PASSWORD + value: pass + - name: POSTGRES_USER + value: root + ports: + - containerPort: 5432 + name: postgres + startupProbe: + exec: + command: [ "pg_isready" ] + failureThreshold: 30 + periodSeconds: 10 + - name: postgres-dev + image: postgres:15.4 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - all + env: + - name: POSTGRES_PASSWORD + value: pass + - name: POSTGRES_USER + value: root + - name: PGPORT + value: "5433" + ports: + - containerPort: 5433 + name: postgres-dev + startupProbe: + exec: + command: [ "pg_isready" ] + failureThreshold: 30 + periodSeconds: 10