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
3 changes: 0 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,4 @@ COPY --from=atlas /usr/local/bin/atlas /usr/local/bin
RUN chmod +x /usr/local/bin/atlas
ENV ATLAS_KUBERNETES_OPERATOR=1
USER 65532:65532
# Workaround for the issue with x/tools/imports
# See: https://github.com/golang/go/issues/75505
ENV HOME=/tmp
ENTRYPOINT ["/manager"]
2 changes: 2 additions & 0 deletions api/v1alpha1/reason.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const (
ReasonCreatingAtlasClient = "CreatingAtlasClient"
// ReasonCreatingWorkingDir represents the reason for creating a working directory.
ReasonCreatingWorkingDir = "CreatingWorkingDir"
// ReasonLogin represents the reason for logging in to Atlas.
ReasonLogin = "Login"
)

// isFailedReason returns true if the given reason is a failed reason.
Expand Down
29 changes: 27 additions & 2 deletions charts/atlas-operator/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,25 @@ spec:
kubectl.kubernetes.io/default-container: manager
{{- end }}
spec:
{{- with .Values.extraVolumes }}
securityContext:
{{- if .Values.persistence.fsGroup }}
fsGroup: {{ .Values.persistence.fsGroup }}
{{- else }}
{{- with .Values.podSecurityContext }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
{{- if or .Values.persistence.enabled .Values.extraVolumes }}
volumes:
{{- if .Values.persistence.enabled }}
- name: data
persistentVolumeClaim:
claimName: {{ include "atlas-operator.fullname" . }}
{{- end }}
{{- with .Values.extraVolumes }}
{{- toYaml . | nindent 6 }}
{{- end }}
{{- end }}
containers:
- image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
Expand All @@ -60,13 +75,23 @@ spec:
value: "{{ .Values.prewarmDevDB }}"
- name: ALLOW_CUSTOM_CONFIG
value: "{{ .Values.allowCustomConfig }}"
{{- if .Values.persistence.enabled }}
- name: DATA_DIR
value: {{ .Values.persistence.mountPath | quote }}
{{- end }}
{{- with .Values.extraEnvs }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.extraVolumeMounts }}
{{- if or .Values.persistence.enabled .Values.extraVolumeMounts }}
volumeMounts:
{{- if .Values.persistence.enabled }}
- name: data
mountPath: {{ .Values.persistence.mountPath }}
{{- end }}
{{- with .Values.extraVolumeMounts }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
Expand Down
23 changes: 23 additions & 0 deletions charts/atlas-operator/templates/pvc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{{- if .Values.persistence.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "atlas-operator.fullname" . }}
labels:
{{- include "atlas-operator.labels" . | nindent 4 }}
{{- with .Values.persistence.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
accessModes:
{{- range .Values.persistence.accessModes }}
- {{ . | quote }}
{{- end }}
{{- if .Values.persistence.storageClassName }}
storageClassName: {{ .Values.persistence.storageClassName | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.size | quote }}
{{- end }}
15 changes: 15 additions & 0 deletions charts/atlas-operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,21 @@ extraEnvs: []
# key: BAR
# name: config-map-resource

# Persistent volume configuration
persistence:
enabled: false
# Storage class name for the PVC
# storageClassName: ""
# Access modes for the PVC
accessModes:
- ReadWriteOnce
# Size of the persistent volume
size: 1Gi
# Mount path inside the container
mountPath: /data
# Annotations for the PVC
annotations: {}

extraVolumes: []
# extraVolumes:
# - name: extra-volume
Expand Down
33 changes: 32 additions & 1 deletion config/manager/manager.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,26 @@ metadata:
app.kubernetes.io/managed-by: kustomize
name: system
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: controller-manager
namespace: system
labels:
control-plane: controller-manager
app.kubernetes.io/name: persistentvolumeclaim
app.kubernetes.io/instance: controller-manager
app.kubernetes.io/component: manager
app.kubernetes.io/created-by: atlas-operator
app.kubernetes.io/part-of: atlas-operator
app.kubernetes.io/managed-by: kustomize
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
Expand Down Expand Up @@ -72,21 +92,29 @@ spec:
# - linux
securityContext:
runAsNonRoot: true
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
# TODO(user): For common cases that do not require escalating privileges
# it is recommended to ensure that all your Pods/Containers are restrictive.
# More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
# Please uncomment the following code if your project does NOT have to work on old Kubernetes
# versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ).
# seccompProfile:
# type: RuntimeDefault
volumes:
- name: data
persistentVolumeClaim:
claimName: controller-manager
containers:
- command:
- /manager
args:
- --leader-elect
image: controller:latest
name: manager
env: []
env:
- name: DATA_DIR
value: "/data"
securityContext:
runAsUser: 1000
allowPrivilegeEscalation: false
Expand Down Expand Up @@ -114,5 +142,8 @@ spec:
requests:
cpu: 250m
memory: 512Mi
volumeMounts:
- name: data
mountPath: /data
serviceAccountName: controller-manager
terminationGracePeriodSeconds: 10
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/ariga/atlas-operator
go 1.25.5

require (
ariga.io/atlas v1.1.1-0.20260308105132-c57f9007e040
ariga.io/atlas v1.1.1-0.20260316181358-9138a9fe419e
github.com/hashicorp/hcl/v2 v2.18.1
github.com/rogpeppe/go-internal v1.13.1
github.com/stretchr/testify v1.11.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
ariga.io/atlas v1.1.1-0.20260308105132-c57f9007e040 h1:L3CUvn8kMRmVFBQoIHhL8zgCbUqjf79cxD9Ideud2NY=
ariga.io/atlas v1.1.1-0.20260308105132-c57f9007e040/go.mod h1:vg7qWSatkNqs04Y4Lheg7vR4bGX0wy51Wz4FIMFVr3U=
ariga.io/atlas v1.1.1-0.20260316181358-9138a9fe419e h1:fbkeTe7KFM7uCoA0I7UPNXtkRMrfCqppm0T6LFuakVc=
ariga.io/atlas v1.1.1-0.20260316181358-9138a9fe419e/go.mod h1:vg7qWSatkNqs04Y4Lheg7vR4bGX0wy51Wz4FIMFVr3U=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
Expand Down
8 changes: 7 additions & 1 deletion internal/controller/atlasmigration_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"fmt"
"io"
"net/url"
"path/filepath"
"runtime"
"strings"

Expand Down Expand Up @@ -268,10 +269,15 @@ func (r *AtlasMigrationReconciler) reconcile(ctx context.Context, data *migratio
return r.resultErr(res, err, "ReadingMigrationData")
}
defer wd.Close()
c, err := r.atlasClient(wd.Path())
c, err := r.atlasClient(wd.Path(), data.Cloud, filepath.Join(res.Namespace, res.Name))
if err != nil {
return r.resultErr(res, err, dbv1alpha1.ReasonCreatingAtlasClient)
}
if data.Cloud != nil && data.Cloud.Token != "" {
if err := c.Login(ctx, &atlasexec.LoginParams{Token: data.Cloud.Token, GrantOnly: true}); err != nil {
return r.resultErr(res, err, dbv1alpha1.ReasonLogin)
}
}
var whoami *atlasexec.WhoAmI
switch whoami, err = c.WhoAmI(ctx, &atlasexec.WhoAmIParams{Vars: data.Vars}); {
case errors.Is(err, atlasexec.ErrRequireLogin):
Expand Down
2 changes: 1 addition & 1 deletion internal/controller/atlasmigration_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -793,7 +793,7 @@ func TestReconcile_reconcile_baseline(t *testing.T) {
t.Cleanup(func() {
require.NoError(t, wd.Close())
})
cli, err := tt.r.atlasClient(wd.Path())
cli, err := tt.r.atlasClient(wd.Path(), md.Cloud, "")
require.NoError(t, err)
report, err := cli.MigrateStatus(context.Background(), &atlasexec.MigrateStatusParams{
Env: "test",
Expand Down
11 changes: 8 additions & 3 deletions internal/controller/atlasschema_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,15 @@ func (r *AtlasSchemaReconciler) Reconcile(ctx context.Context, req ctrl.Request)
_, err = wd.WriteFile("atlas.hcl", buf.Bytes())
return err
}
cli, err := r.atlasClient(wd.Path())
cli, err := r.atlasClient(wd.Path(), data.Cloud, filepath.Join(res.Namespace, res.Name))
if err != nil {
return r.resultErr(res, err, dbv1alpha1.ReasonCreatingAtlasClient)
}
if data.Cloud != nil && data.Cloud.Token != "" {
if err := cli.Login(ctx, &atlasexec.LoginParams{Token: data.Cloud.Token, GrantOnly: true}); err != nil {
return r.resultErr(res, err, dbv1alpha1.ReasonLogin)
}
}
// Calculate the hash of the current schema.
hash, err := cli.SchemaInspect(ctx, &atlasexec.SchemaInspectParams{
Env: data.EnvName,
Expand Down Expand Up @@ -346,7 +351,7 @@ func (r *AtlasSchemaReconciler) Reconcile(ctx context.Context, req ctrl.Request)
}); err != nil {
return r.resultErr(res, err, "ModifyingAtlasHCL")
}
err = r.lint(ctx, wd, data, data.Vars)
err = r.lint(ctx, wd, data, data.Vars, res.Namespace, res.Name)
switch d := (*destructiveErr)(nil); {
case errors.As(err, &d):
reason, msg := d.FirstRun()
Expand All @@ -372,7 +377,7 @@ func (r *AtlasSchemaReconciler) Reconcile(ctx context.Context, req ctrl.Request)
})
// Run the linting policy.
case shouldLint:
if err = r.lint(ctx, wd, data, nil); err != nil {
if err = r.lint(ctx, wd, data, nil, res.Namespace, res.Name); err != nil {
return r.resultCLIErr(res, err, "LintPolicyError")
}
reports, err = cli.SchemaApplySlice(ctx, &atlasexec.SchemaApplyParams{
Expand Down
8 changes: 4 additions & 4 deletions internal/controller/atlasschema_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ func TestSchemaConfigMap(t *testing.T) {
require.NoError(t, err)

// Assert that the schema was applied.
cli, err := tt.r.atlasClient("")
cli, err := tt.r.atlasClient("", nil, "")
require.NoError(t, err)
inspect, err := cli.SchemaInspect(ctx, &atlasexec.SchemaInspectParams{
URL: tt.dburl,
Expand Down Expand Up @@ -489,7 +489,7 @@ func Test_FirstRunDestructive(t *testing.T) {
require.Contains(t, ev, "FirstRunDestructive")
require.Contains(t, ev, "Warning")

cli, err := tt.r.atlasClient("")
cli, err := tt.r.atlasClient("", nil, "")
require.NoError(t, err)
ins, err := cli.SchemaInspect(context.Background(), &atlasexec.SchemaInspectParams{
URL: tt.dburl,
Expand Down Expand Up @@ -538,7 +538,7 @@ func TestDiffPolicy(t *testing.T) {
tt.initDB("create table x (c int);")
_, err := tt.r.Reconcile(context.Background(), req())
require.NoError(t, err)
cli, err := tt.r.atlasClient("")
cli, err := tt.r.atlasClient("", nil, "")
require.NoError(t, err)
ins, err := cli.SchemaInspect(context.Background(), &atlasexec.SchemaInspectParams{
URL: tt.dburl,
Expand Down Expand Up @@ -717,7 +717,7 @@ type test struct {
func cliTest(t *testing.T) *test {
tt := newTest(t)
var err error
tt.r.atlasClient = func(dir string) (AtlasExec, error) {
tt.r.atlasClient = func(dir string, c *Cloud, _ string) (AtlasExec, error) {
cli, err := atlasexec.NewClient(dir, "atlas")
if err != nil {
return nil, err
Expand Down
46 changes: 40 additions & 6 deletions internal/controller/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"io"
"iter"
"maps"
"os"
"path/filepath"
"slices"
"strings"
"time"
Expand Down Expand Up @@ -67,14 +69,16 @@ type (
SchemaPlanList(context.Context, *atlasexec.SchemaPlanListParams) ([]atlasexec.SchemaPlanFile, error)
// WhoAmI runs the `whoami` command.
WhoAmI(context.Context, *atlasexec.WhoAmIParams) (*atlasexec.WhoAmI, error)
// Login runs the `login` command (e.g. with --grant-only for offline tokens).
Login(context.Context, *atlasexec.LoginParams) error
// SetStdout specifies a writer to stream stdout to for every command.
SetStdout(io.Writer)
// SetStderr specifies a writer to stream stderr to for every command.
SetStderr(io.Writer)
}
// AtlasExecFn is a function that returns an AtlasExec
// with the working directory.
AtlasExecFn func(string) (AtlasExec, error)
// AtlasExecFn is a function that returns an AtlasExec configured
// with the given working directory, cloud configuration, and HOME directory.
AtlasExecFn func(dir string, c *Cloud, home string) (AtlasExec, error)
// Cloud holds the cloud configuration.
Cloud struct {
Token string
Expand All @@ -83,10 +87,40 @@ type (
}
)

// NewAtlasExec returns a new AtlasExec with the given working-directory.
const (
// envDataDir is the environment variable for the data directory.
envDataDir = "DATA_DIR"
)

// NewAtlasExec returns a new AtlasExec with the given directory and cloud configuration.
// The atlas binary is expected to be in the $PATH.
func NewAtlasExec(dir string) (AtlasExec, error) {
return atlasexec.NewClient(dir, "atlas")
// If DATA_DIR is set, it creates a resource-specific directory and sets HOME to it,
// allowing Atlas CLI to store its data (.atlas) under the mounted PVC.
func NewAtlasExec(dir string, c *Cloud, home string) (AtlasExec, error) {
cli, err := atlasexec.NewClient(dir, "atlas")
if err != nil {
return nil, err
}
env := atlasexec.NewOSEnviron()
// If DATA_DIR is set, create a resource-specific directory and set HOME.
if dataDir := os.Getenv(envDataDir); dataDir != "" {
homeDir := filepath.Join(dataDir, home)
if err := os.MkdirAll(homeDir, 0755); err != nil {
return nil, fmt.Errorf("creating resource home directory: %w", err)
}
env["HOME"] = homeDir
} else if env["HOME"] == "" {
// Ensure HOME is set to a safe default when not provided by the environment
// and no DATA_DIR-based home directory is configured.
env["HOME"] = "/tmp"
}
if c != nil && c.Token != "" {
env["ATLAS_TOKEN"] = c.Token
}
if err = cli.SetEnv(env); err != nil {
return nil, err
}
return cli, nil
}

func getConfigMap(ctx context.Context, r client.Reader, ns string, ref *corev1.LocalObjectReference) (*corev1.ConfigMap, error) {
Expand Down
Loading
Loading