Migrate Airflow provider imports and cache loading¶
Purpose: move an existing dpone Airflow installation to the canonical provider, fail-closed generated loader, and immutable release/deployment cache without adding network or database access to DAG parsing.
Audience: Airflow platform engineers and DAG repository maintainers upgrading
legacy dpone_airflow_pack imports or a mutable latest/pack-index.json cache.
Use this guide for an upgrade. For a new installation, start with the lightweight provider overview and use the canonical loader from the beginning.
Safe target¶
| Surface | Legacy compatibility path | Target |
|---|---|---|
| Provider facade import | dpone_airflow_pack |
airflow.providers.dpone |
| Generated loader | Return value ignored, or no fatal guard | Assign LoadReport, pass index_path, and raise with the first stable error code when fatal |
| Cache source | Remote mutable latest/pack-index.json |
Local current/airflow-index.json pointing at one sealed immutable deployment |
| Artifact identity | Mutable generation/current lookup | Pinned release_id, deployment_id, positive bytes, and SHA-256 |
| DAG parse I/O | A sync sidecar may update legacy state | Provider reads bounded local files only; no network, metadata DB, Variables, Connections, Vault, or Kubernetes |
| Runtime delivery | v1 local_preview or incomplete v1 init_fetch |
Keep v1 local_preview non-runnable; regenerate executable init_fetch as a complete v2 index |
current/airflow-index.json is a local activation path, not a mutable artifact
identity. The cache promoter switches current atomically to a sealed
deployment. Every artifact reference inside the index remains pinned below an
immutable release and must not contain current or latest.
Prerequisites¶
- Build one scheduler image with
apache-airflow-providers-dponeand its matchingdpone-airflow-packdependency on the same dpone release line as the generated runtime packs. - Mount the new cache at
/opt/airflow/dags/.dpone-cache, or choose another stable local mount and use that exact absolute path inindex_path. - Retain the last known-good release and deployment until cutover verification and the rollback window complete.
- Run publication, remote materialization, promotion, and recovery outside the scheduler/DAG parse process under a platform identity.
- Stop if the old cache has no trusted immutable release source. Do not convert currently visible mutable bytes into a release by editing JSON or copying a pack in place.
1. Move provider facade imports¶
Replace provider-facade imports as one mechanical change:
| Legacy import | Canonical import |
|---|---|
from dpone_airflow_pack import load_dpone_dags |
from airflow.providers.dpone import load_dpone_dags |
from dpone_airflow_pack import DponeDag, DponeTaskGroup |
from airflow.providers.dpone import DponeDag, DponeTaskGroup |
from dpone_airflow_pack import CacheResolver, LoadReport |
from airflow.providers.dpone import CacheResolver, LoadReport |
from dpone_airflow_pack import AirflowDeploymentIndex, AirflowDeploymentIndexError, AirflowIndexArtifact |
from airflow.providers.dpone import AirflowDeploymentIndex, AirflowDeploymentIndexError, AirflowIndexArtifact |
The legacy root exports remain compatibility shims and emit one
DeprecationWarning per process. Pack-library helpers such as
load_dpone_airflow_pack_with_provenance,
workload_ids_from_gitops_domain, and runtime-adapter helpers still belong to
dpone_airflow_pack; they are not provider-facade imports.
Verify the scheduler image locally:
This import check performs no remote cache refresh or database access.
2. Initialize or upgrade the generated loader¶
From the authoring repository, run:
The command initializes dags/dpone.py. On a rerun, it upgrades only exact
known fingerprints of historical dpone-generated loader bytes. Canonical bytes
are an idempotent no-op. A loader with any user change, including comments or
whitespace, is a conflict and remains byte-for-byte unchanged; review and
migrate that file manually instead of forcing an overwrite.
The target loader is:
from airflow.providers.dpone import load_dpone_dags
load_report = load_dpone_dags(
globals(),
index_path="/opt/airflow/dags/.dpone-cache/current/airflow-index.json",
)
if load_report.fatal:
error_code = (
load_report.errors[0].get("code", "DPONE_AIRFLOW_INDEX_INVALID")
if load_report.errors
else "DPONE_AIRFLOW_INDEX_INVALID"
)
raise RuntimeError(
f"{error_code}: dpone Airflow deployment index could not be loaded"
)
Keep index_path explicit and aligned with the scheduler image mount. The
loader assigns the returned LoadReport before checking fatal; it preserves
errors[0].code when available and uses
DPONE_AIRFLOW_INDEX_INVALID only as the defensive fallback. It does not fetch
the index, resolve credentials, or query the Airflow metadata database.
Review and deploy the generated file through the normal DAG repository workflow. Do not add cache sync, object-storage, SQL, Variable, Connection, Vault, or Kubernetes calls to it.
3. Replace the mutable pack index¶
The legacy compatibility command may read a URL such as:
Do not point the provider loader at that object and do not fetch it during DAG parse. Build or obtain a complete immutable release/deployment pair, then use the platform publication and materialization workflow documented in Airflow cache sync and recovery. The resulting local layout is:
.dpone-cache/
releases/sha256-<release>/
release-set.json
dags/...
packs/...
deployments/<environment>/sha256-<deployment>/
deployment.json
airflow-index.json
_SUCCESS
activations/<environment>/sha256-<deployment>/
deployment.json
airflow-index.json
_SUCCESS
current -> activations/<environment>/sha256-<deployment>
current must be exactly one relative symlink to the canonical sealed
activation path. The provider rejects a directory, an absolute symlink, a
deployments/** target, traversal, a symlinked intermediate component, a
non-lowercase digest, or a deployment/environment identity mismatch. Existing
caches with another layout must be rematerialized and promoted with
dpone airflow cache-sync; never repair immutable bytes or the symlink by hand.
The new airflow-index.json must use the wire that matches its purpose:
dpone.airflow-deployment-index.v1remains the supported non-runnablelocal_previewcompatibility wire;dpone.airflow-deployment-index.v2is required for executableinit_fetch.
Both use exact lowercase SHA-256 release and deployment identities and pinned
cache://releases/sha256-<release>/... artifact references. Do not change only
the schema string: v2 is a new immutable deployment projection with a complete
image, artifact, trust, identity, and runtime-plan contract.
After the exact release and deployment have been materialized below the new cache root, perform the first reviewed promotion:
The stock hardening candidate has no concrete production attestation verifier.
A production required_for_prod projection may therefore be promoted only as a
paused production parse canary: its tasks must not be triggered. Runnable
production promotion requires the configured verifier plus current live
Kubernetes/workload-identity certification. For a non-production executable
rehearsal, build a non_production deployment in dev/stage instead of weakening
the production policy.
: "${DPONE_SCHEDULER_CACHE_ROOT:=/opt/airflow/dags/.dpone-cache}"
: "${DPONE_DEPLOYMENT_DIR:?set the reviewed sha256-... deployment directory name}"
dpone airflow cache-sync \
--cache-root "${DPONE_SCHEDULER_CACHE_ROOT}" \
--deployment-dir "${DPONE_SCHEDULER_CACHE_ROOT}/deployments/prod/${DPONE_DEPLOYMENT_DIR}" \
--environment prod \
--promoted-by ci://your-platform/dpone-airflow \
--allowed-promoter ci://your-platform/dpone-airflow \
--expect-current-absent \
--confirm-promote
If the new cache already has a current deployment, generate a fresh
cache-recovery-plan and use its current_path_deployment_id as
--expected-current-deployment-id; do not reuse the first-promotion command.
Promotion verifies local release/index/artifact identities and then activates a
sealed snapshot. It does not download remote objects.
Keep the legacy sync and new cache in separate directories during a canary.
After the new loader parses the expected DAG set from
current/airflow-index.json, remove the legacy dpone-airflow-pack-sync
sidecar or startup step. Do not let it write into the new cache root.
Migrate v1 init-fetch to v2¶
First classify the active local index without contacting Airflow or a remote registry:
jq -r '[.schema, .runtime_artifact_delivery.mode] | @tsv' \
/opt/airflow/dags/.dpone-cache/current/airflow-index.json
Apply this decision table:
| Observed index | Action |
|---|---|
v1 local_preview |
No executable migration is required. Keep it on the non-runnable preview lane. |
v1 init_fetch |
Stop. The provider returns DPONE_RUNTIME_ARTIFACT_DELIVERY_MIGRATION_REQUIRED before installing a DAG. |
v2 init_fetch |
Continue only after validating the complete fields and compatible provider/runtime image. |
v2 non-init_fetch |
Stop with DPONE_RUNTIME_ARTIFACT_DELIVERY_MODE_UNSUPPORTED. |
For v1 init_fetch, regenerate the immutable release/deployment projection with
a v2-capable producer. The resulting v2 index must include:
- an exact
runtime_image_refwith the same digest asruntime_image_digest; - matching explicit top-level and delivery-level
trust_tier; - positive
bytesand SHA-256 for the exact release, deployment, and every selected workload pack, plus each pack fingerprint; pack_identity.schema: dpone.airflow-pack-identity.v1in every selected workload pack, with a fingerprint independently derived from the complete canonical pack rather than copied from release metadata;- a bounded logical registry ref, a workload-identity service account and
namespace, and a digest-pinned registry ConfigMap
{kind, name, key, sha256}; - for
production, a digest-pinned trust-policy ConfigMap andattestations: required_for_prod.
Each selected workload pack must be rebuilt by a producer that emits the
whole-pack identity, the closed
dpone.airflow-provider-execution.v1 projection, and a digest-pinned XCom
sidecar image. Check the structural declarations in the materialized release
without executing it:
: "${DPONE_RELEASE_DIR:?set the reviewed immutable release directory}"
find "${DPONE_RELEASE_DIR}/packs" -type f -name '*.json' -print0 |
xargs -0 -n1 jq -e \
'(.pack_identity.schema == "dpone.airflow-pack-identity.v1")
and (.pack_fingerprint | test("^sha256:[0-9a-f]{64}$"))
and (.provider_execution.schema == "dpone.airflow-provider-execution.v1")
and (.xcom.sidecar_image | test("@sha256:[0-9a-f]{64}$"))'
This jq command is only a structural preflight. The build plane, provider and
runtime still recompute the whole-pack identity from the exact bytes.
If the installed producer still emits v1 for init_fetch, cannot accept these
deployment-owned inputs, or emits packs without the canonical identity, strict
projection or pinned sidecar, stop the cutover and upgrade the producer. A
missing strict execution field returns a migration-required error before DAG
installation. Never copy fields into a published index/pack, rename v1 to v2,
or calculate missing sizes or fingerprints by editing immutable JSON. Those
edits invalidate pack, release and deployment identity and do not create the
structured runtime payload required by the no-shell launcher.
Complete v2 index example¶
The following document is a complete, schema-valid structural example. Its
digests, byte counts, and names are illustrative: obtain the real file from
dpone airflow build and immutable publication instead of editing this example
into a deployed cache.
{
"schema": "dpone.airflow-deployment-index.v2",
"release_id": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"deployment_id": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"trust_tier": "production",
"dag_specs": [
{
"id": "orders_daily",
"artifact_ref": "cache://releases/sha256-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/dags/orders_daily.dag-spec.json",
"sha256": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"bytes": 4096
}
],
"workload_packs": [
{
"id": "load_orders",
"artifact_ref": "cache://releases/sha256-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/packs/load_orders.airflow-pack.json",
"sha256": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
"bytes": 8192,
"pack_fingerprint": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
}
],
"binding_set_ref": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
"connection_registry_ref": "sha256:2222222222222222222222222222222222222222222222222222222222222222",
"credential_runtime_ref": "sha256:3333333333333333333333333333333333333333333333333333333333333333",
"binding_set": {
"artifact_ref": "cache://runtime-connection-contexts/sha256-9999999999999999999999999999999999999999999999999999999999999999/binding-set.json",
"sha256": "sha256:8888888888888888888888888888888888888888888888888888888888888888",
"bytes": 1024
},
"connection_registry": {
"artifact_ref": "cache://runtime-connection-contexts/sha256-9999999999999999999999999999999999999999999999999999999999999999/connection-registry.json",
"sha256": "sha256:7777777777777777777777777777777777777777777777777777777777777777",
"bytes": 2048
},
"credential_runtime": {
"artifact_ref": "cache://runtime-connection-contexts/sha256-9999999999999999999999999999999999999999999999999999999999999999/credential-runtime.json",
"sha256": "sha256:6666666666666666666666666666666666666666666666666666666666666666",
"bytes": 1536
},
"runtime_image_ref": "registry.example/dpone/runtime@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"runtime_image_digest": "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"airflow_bundle_ref": "git:7ac31f2",
"runtime_artifact_delivery": {
"mode": "init_fetch",
"trust_tier": "production",
"artifact_registry_ref": "dpone-prod-artifacts",
"identity": {
"method": "kubernetes_workload_identity",
"service_account": "dpone-runtime",
"namespace": "airflow-dwh"
},
"registry_config_ref": {
"kind": "kubernetes_config_map",
"name": "dpone-artifact-registry",
"key": "registry.json",
"sha256": "sha256:4444444444444444444444444444444444444444444444444444444444444444"
},
"trust_policy_ref": {
"kind": "kubernetes_config_map",
"name": "dpone-artifact-trust-policy",
"key": "policy.json",
"sha256": "sha256:5555555555555555555555555555555555555555555555555555555555555555"
},
"source": {
"artifact_registry_ref": "dpone-prod-artifacts"
},
"verify": {
"checksums": "required",
"attestations": "required_for_prod"
}
},
"release": {
"artifact_ref": "cache://releases/sha256-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/release-set.json",
"sha256": "sha256:6666666666666666666666666666666666666666666666666666666666666666",
"bytes": 16384
},
"deployment": {
"artifact_ref": "cache://deployments/prod/sha256-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb/deployment.json",
"sha256": "sha256:7777777777777777777777777777777777777777777777777777777777777777",
"bytes": 12288
}
}
Publish, materialize, and promote the regenerated release/deployment as new immutable identities. Keep provider and runtime image on the same contract line. During canary review, verify:
- the active index is v2
init_fetchand its mirrored trust tiers match; - every selected pack has a verified whole-pack identity, the closed
dpone.airflow-provider-execution.v1projection, and a digest-pinned XCom sidecar image; - the provider constructs fixed
runtime-init-fetchandruntime-pack-execcommands with empty arguments; - image, service account, namespace, reserved volumes, and mounts come from the v2 delivery context rather than the pack projection;
- the registry and optional trust-policy ConfigMap names/keys match the plan and their mounted bytes match the pinned digests;
- production without a configured verifier fails
DPONE_ARTIFACT_ATTESTATION_REQUIREDbefore registry I/O.
Items 1-4 can be established with local contract/model evidence. ConfigMap
projection, workload identity, registry access, and attestation require the
real approved Kubernetes environment; record them as UNVERIFIED when that
environment was not exercised.
Compatibility warning: v1 missing bytes and strict writers¶
Current deployment-index writers and the public v1 JSON Schema require every
dag_specs[] and workload_packs[] entry to contain a positive bytes value
alongside sha256.
The provider reader temporarily accepts a legacy entry with missing bytes.
It emits one DeprecationWarning per process, reads the artifact through the
configured byte ceiling, verifies its SHA-256, and records the actual size in
the in-memory index. This is read compatibility only:
- new writers must remain strict and always emit positive
bytes; - schema validation, build, and publication reject a legacy index even though the provider compatibility reader can consume it;
- do not suppress the warning or hand-add sizes to a published index, because index and deployment identities would no longer describe the same bytes;
- regenerate the release/deployment index with the current preview/build producer, then publish, materialize, and promote the new immutable identity.
Treat the warning as an upgrade deadline, not certification evidence.
The v2 executable reader has no missing-bytes compatibility path: all exact
release, deployment, and workload descriptors are strict.
In a canary scheduler image, make the normally filtered deprecation visible while verifying only local bytes:
PYTHONWARNINGS=default::DeprecationWarning \
python -c "from airflow.providers.dpone import load_airflow_deployment_index; load_airflow_deployment_index('/opt/airflow/dags/.dpone-cache/current/airflow-index.json')"
Verify cutover¶
- Confirm the generated loader and scheduler mount use the same explicit
index_path. - Run the read-only local planner:
dpone airflow cache-recovery-plan \
--cache-root /opt/airflow/dags/.dpone-cache \
--environment prod \
--format json
- Parse the DAG file in the candidate scheduler image. Verify
load_report.fatalis false, the expected DAG IDs are loaded, and any isolatederrors[]identify the affecteddag_id. - For executable delivery, confirm the active schema is v2
init_fetch; a v1init_fetchmust produce the migration-required error instead of a KPO. - With deprecation warnings enabled for the canary, confirm scheduler/DAG
processor logs contain no legacy missing-
byteswarning and no remote sync, metadata DB, Variable, Connection, Vault, or Kubernetes access from the loader. - Keep the previous complete deployment protected from retention until the rollback window closes.
An unavailable live Airflow cluster is not a pass. Record cluster parsing as
UNVERIFIED and retain the local provider/import/cache evidence separately.
Recover a failed cutover¶
If the loader raises, preserve the first stable error code. A fatal
DPONE_AIRFLOW_INDEX_* error means no trusted deployment snapshot exists; do
not remove the guard or treat zero loaded DAGs as success.
Start with the read-only planner:
dpone airflow cache-recovery-plan \
--cache-root /opt/airflow/dags/.dpone-cache \
--environment prod \
--format json
Rematerialize the exact immutable release/deployment from trusted storage when the plan reports missing or corrupt content. If the plan reports a repairable split pointer/current state, apply only its reviewed candidate and fresh CAS guard:
: "${DPONE_REPAIR_DEPLOYMENT_ID:?set the reviewed repair deployment sha256 digest}"
: "${DPONE_CURRENT_DEPLOYMENT_ID:?set the reviewed current deployment sha256 digest}"
dpone airflow cache-recovery-apply \
--cache-root /opt/airflow/dags/.dpone-cache \
--environment prod \
--deployment-id "${DPONE_REPAIR_DEPLOYMENT_ID}" \
--promoted-by ci://your-platform/dpone-airflow-recovery \
--allowed-promoter ci://your-platform/dpone-airflow-recovery \
--expected-current-deployment-id "${DPONE_CURRENT_DEPLOYMENT_ID}" \
--confirm-repair
Use --expect-current-absent only when the fresh plan confirms that the active
path is absent and repairable. Never edit the active index, pointer, DAG spec,
or workload pack in place.
Rollback¶
Rollback means re-promoting a retained, complete immutable deployment; there is
no mutable latest rollback and no reason to restore a loader that can hide a
fatal index failure.
For a healthy current state, use the current deployment ID from a fresh recovery plan as the CAS guard and promote the previous deployment:
: "${DPONE_PREVIOUS_DEPLOYMENT_DIR:?set the reviewed sha256-... deployment directory name}"
: "${DPONE_CURRENT_DEPLOYMENT_ID:?set the reviewed current deployment sha256 digest}"
dpone airflow cache-sync \
--cache-root /opt/airflow/dags/.dpone-cache \
--deployment-dir "/opt/airflow/dags/.dpone-cache/deployments/prod/${DPONE_PREVIOUS_DEPLOYMENT_DIR}" \
--environment prod \
--promoted-by ci://your-platform/dpone-airflow \
--allowed-promoter ci://your-platform/dpone-airflow \
--expected-current-deployment-id "${DPONE_CURRENT_DEPLOYMENT_ID}" \
--confirm-promote
If pointer and active state disagree, use cache-recovery-plan and
cache-recovery-apply instead of promotion. If the scheduler image itself must
be rolled back, deploy the matching provider/pack release line together and
keep the explicit index_path plus fatal guard. Stop when the retained
deployment or its pinned release cannot be verified; do not reconstruct it
from mutable legacy cache bytes.
Do not use an incomplete v1 init_fetch index as an executable rollback
target. A retained v1 local_preview remains useful only for non-runnable
preview. Executable rollback must select another complete v2 init_fetch
deployment whose provider and runtime image understand the same wire.
After rollback, rerun the recovery plan and verify the expected DAG IDs. Keep the failed deployment for incident evidence until retention policy explicitly allows deletion.