Skip to content
Open
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: 2 additions & 0 deletions .changes/unreleased/+base-only-direct-install.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
kind: Fixed
body: Preserve resolved base-image identity when building an application runtime layer for environments without package-provider layers.
14 changes: 14 additions & 0 deletions internal/dockerdeploy/install_runtime_identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,20 @@ func buildInstalledRuntimeIdentityWithV1(
if err != nil {
return installedRuntimeIdentityBuildV1{}, fmt.Errorf("inspect installed runtime identity upstream: %w", err)
}
baseImage, err := realizedImageFromDescriptor(source.Lock.Base)
if err != nil {
return installedRuntimeIdentityBuildV1{}, fmt.Errorf("resolve installed runtime identity base: %w", err)
}
if source.Lock.RuntimeLayer.Upstream == baseImage {
if upstream.Image.ConfigDigest != baseImage.ConfigDigest || upstream.Image.RootFSSubject != baseImage.RootFSSubject {
return installedRuntimeIdentityBuildV1{}, fmt.Errorf("installed runtime identity upstream no longer matches the staged build")
}
// Inspection by config ID proves that the selected base still exists,
// but Docker reports it using a local config-ID descriptor. Restore the
// locked registry identity before the account-specific runtime rebuild.
upstream.Descriptor = source.Lock.Base
upstream.Image = baseImage
}
if upstream.Image != source.Lock.RuntimeLayer.Upstream {
return installedRuntimeIdentityBuildV1{}, fmt.Errorf("installed runtime identity upstream no longer matches the staged build")
}
Expand Down
25 changes: 23 additions & 2 deletions internal/dockerdeploy/install_runtime_identity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,23 @@ func TestValidateInstalledRuntimeIdentityBuildV1RejectsWrongPlannedAccount(t *te

func TestBuildInstalledRuntimeIdentityV1RebuildsChangedAccount(t *testing.T) {
current, _ := currentBuildReuseFixture(t)
current.Lock.Base.AuthorReference = "debian:bookworm-slim"
current.Lock.Base.ImmutableReference = "debian@" + string(rendererDigest("a"))
current.Lock.Base.ManifestDigest = rendererDigest("a")
baseImage, err := realizedImageFromDescriptor(current.Lock.Base)
if err != nil {
t.Fatal(err)
}
current.Lock.RuntimeLayer.Upstream = baseImage
current.Lock.RuntimeLayer.TransactionDigest, err = deploy.ApplicationRuntimeLayerTransactionDigestV1(
current.Lock.RuntimeLayer.Verifier,
current.Lock.RuntimeLayer.Account,
baseImage,
current.Lock.Platform,
)
if err != nil {
t.Fatal(err)
}
store, err := providerstore.NewStore(t.TempDir())
if err != nil {
t.Fatal(err)
Expand All @@ -69,13 +86,17 @@ func TestBuildInstalledRuntimeIdentityV1RebuildsChangedAccount(t *testing.T) {
if candidate.ImageID != current.Lock.RuntimeLayer.Upstream.ConfigDigest || platform != current.Lock.Platform {
t.Fatalf("upstream inspection = %#v / %#v", candidate, platform)
}
return InspectedImageCandidate{Image: current.Lock.RuntimeLayer.Upstream}, nil
localDescriptor := current.Lock.Base
localDescriptor.AuthorReference = string(localDescriptor.ConfigDigest)
localDescriptor.ImmutableReference = string(localDescriptor.ConfigDigest)
localDescriptor.ManifestDigest = ""
return inspectedValidationCandidate(t, localDescriptor), nil
},
finalize: func(_ context.Context, gotStore providerstore.Store, layers []FullImageValidationInput, final FullImageValidationInput, validate providers.RequirementProfileOwnerValidator, run FullImageValidationRunner, verifier deploy.ApplicationStartupVerifierV1, account deploy.ApplicationLocalAccountV1, _ RunOptions) (FinalizedBuildValidationResult, error) {
if gotStore.Root() != store.Root() || len(layers) != 0 || validate == nil || run == nil || verifier != current.Lock.RuntimeLayer.Verifier || account != wantAccount {
t.Fatalf("finalization inputs were not preserved")
}
if final.Image.Image != current.Lock.RuntimeLayer.Upstream || !reflect.DeepEqual(final.Outputs, current.Lock.Catalog) || !reflect.DeepEqual(final.RuntimePolicy, current.Lock.RuntimePolicy) {
if !reflect.DeepEqual(final.Image.Descriptor, current.Lock.Base) || final.Image.Image != current.Lock.RuntimeLayer.Upstream || !reflect.DeepEqual(final.Outputs, current.Lock.Catalog) || !reflect.DeepEqual(final.RuntimePolicy, current.Lock.RuntimePolicy) {
t.Fatalf("final validation input = %#v", final)
}
layer := current.Lock.RuntimeLayer
Expand Down
6 changes: 3 additions & 3 deletions internal/dockerdeploy/materialization_layer.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,11 +301,11 @@ func ValidateInspectedImageCandidateIdentity(candidate InspectedImageCandidate)
if err := candidate.Descriptor.Validate(); err != nil {
return fmt.Errorf("validate inspected materialization descriptor: %w", err)
}
rootFSSubject, err := deploy.RootFSSubject(candidate.Descriptor.RootFSDiffIDs)
expected, err := realizedImageFromDescriptor(candidate.Descriptor)
if err != nil {
return fmt.Errorf("validate inspected materialization rootfs: %w", err)
return fmt.Errorf("validate inspected materialization descriptor image: %w", err)
}
if candidate.Image.Digest != candidate.Descriptor.ConfigDigest || candidate.Image.ConfigDigest != candidate.Descriptor.ConfigDigest || candidate.Image.RootFSSubject != rootFSSubject {
if candidate.Image != expected {
return fmt.Errorf("inspected materialization image identity does not match its Docker descriptor")
}
return nil
Expand Down
17 changes: 17 additions & 0 deletions internal/dockerdeploy/materialization_layer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,23 @@ func TestValidateInspectedMaterializationCandidateChecksControlledValuesOnly(t *
}
}

func TestValidateInspectedImageCandidateIdentityRequiresCanonicalDescriptorImage(t *testing.T) {
descriptor := providerBaseDescriptor(t, true)
image, err := realizedImageFromDescriptor(descriptor)
if err != nil {
t.Fatal(err)
}
candidate := InspectedImageCandidate{Descriptor: descriptor, Image: image}
if err := ValidateInspectedImageCandidateIdentity(candidate); err != nil {
t.Fatal(err)
}

candidate.Image.Digest = descriptor.ConfigDigest
if err := ValidateInspectedImageCandidateIdentity(candidate); err == nil || !strings.Contains(err.Error(), "identity") {
t.Fatalf("noncanonical registry image error = %v", err)
}
}

func commandOption(t *testing.T, args []string, option string) string {
t.Helper()
for index := range args {
Expand Down
7 changes: 7 additions & 0 deletions internal/dockerdeploy/provider_graph_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ func prepareProviderGraphValidation(
if candidate.Image.RootFSSubject != graph.PrefixImages[0].RootFSSubject || candidate.Image.ConfigDigest != graph.PrefixImages[0].ConfigDigest {
return ProviderGraphValidationPlan{}, fmt.Errorf("provider graph base changed after resolution")
}
// Inspection by config ID proves that the immutable filesystem and
// configuration still exist, but Docker reports a config-ID descriptor
// instead of the resolved registry descriptor. Restore the exact base
// identity so the inspected candidate remains internally consistent and
// the application runtime layer connects to the graph's base prefix.
candidate.Descriptor = base
candidate.Image = graph.PrefixImages[0]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Normalize registry identity before install-time account rebuild

For a registry-backed base-only graph, this makes RuntimeLayer.Upstream.Digest the manifest digest. If installation changes the local account—most notably a Linux system-scope install from the temporary staged build—buildInstalledRuntimeIdentityWithV1 reinspects that upstream by config ID; InspectBuiltImageCandidate reports Digest == ConfigDigest, so the strict comparison in install_runtime_identity.go:58-67 rejects the unchanged image before rebuilding the runtime identity. Normalize that reinspection back to the locked base identity and descriptor so these installs do not fail.

Useful? React with 👍 / 👎.

final = FullImageValidationInput{
Image: candidate, Profiles: []providers.RequirementProfile{},
Outputs: append([]providers.RealizedOutput{}, baseCatalog...), RuntimePolicy: policy,
Expand Down
10 changes: 7 additions & 3 deletions internal/dockerdeploy/provider_graph_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func TestPrepareProviderGraphValidationInspectsBaseOnlyGraph(t *testing.T) {
if err != nil {
t.Fatal(err)
}
descriptor := providerBaseDescriptor(t, false)
descriptor := providerBaseDescriptor(t, true)
baseImage, err := realizedImageFromDescriptor(descriptor)
if err != nil {
t.Fatal(err)
Expand All @@ -94,13 +94,17 @@ func TestPrepareProviderGraphValidationInspectsBaseOnlyGraph(t *testing.T) {
if candidate.ImageID != descriptor.ConfigDigest {
t.Fatalf("base inspection candidate = %#v", candidate)
}
return inspectedValidationCandidate(t, descriptor), nil
inspectedDescriptor := descriptor
inspectedDescriptor.AuthorReference = string(descriptor.ConfigDigest)
inspectedDescriptor.ImmutableReference = string(descriptor.ConfigDigest)
inspectedDescriptor.ManifestDigest = ""
return inspectedValidationCandidate(t, inspectedDescriptor), nil
},
)
if err != nil {
t.Fatal(err)
}
if len(result.Layers) != 0 || result.Final.Image.Image != baseImage {
if len(result.Layers) != 0 || result.Final.Image.Image != baseImage || !reflect.DeepEqual(result.Final.Image.Descriptor, descriptor) {
t.Fatalf("base-only validation plan = %#v", result)
}
}
Expand Down
Loading