@@ -329,6 +329,37 @@ fn apply_repo_version_spec(
329329 sanitize_name_component( & spec. org) ,
330330 sanitize_name_component( & spec. repo)
331331 ) ;
332+
333+ // Delete any existing render Function so Crossplane re-resolves with the
334+ // correct digest for this version (avoids conflicts when switching between
335+ // local and published builds).
336+ let render_source = format ! ( "ghcr.io/{}/{}_render" , spec. org, spec. repo) ;
337+ let sources: HashSet < String > = [ render_source. clone ( ) ] . into_iter ( ) . collect ( ) ;
338+ let removed = delete_package_resources_by_source ( "function.pkg.crossplane.io" , & sources) ?;
339+ if removed > 0 {
340+ log:: info!( "Deleted {} stale Function package(s) before version install" , removed) ;
341+ }
342+
343+ // Delete any local-registry ImageConfig rewrite left over from a previous
344+ // `config install --path` so Crossplane pulls from ghcr.io.
345+ let ic_name = image_config_name ( & render_source) ;
346+ let ic_check = Command :: new ( "kubectl" )
347+ . args ( [ "get" , "imageconfig.pkg.crossplane.io" , & ic_name] )
348+ . stdout ( Stdio :: null ( ) )
349+ . stderr ( Stdio :: null ( ) )
350+ . status ( ) ;
351+ if ic_check. map ( |s| s. success ( ) ) . unwrap_or ( false ) {
352+ run_cmd (
353+ "kubectl" ,
354+ & [ "delete" , "imageconfig.pkg.crossplane.io" , & ic_name] ,
355+ ) ?;
356+ log:: info!( "Deleted local ImageConfig rewrite '{}'" , ic_name) ;
357+ }
358+
359+ // Delete stale inactive ConfigurationRevisions pointing at the local
360+ // registry so they don't block dependency resolution for the published version.
361+ delete_local_registry_config_revisions ( & config_name) ?;
362+
332363 apply_configuration (
333364 & config_name,
334365 & package_ref,
@@ -516,7 +547,12 @@ fn run_local_path(
516547 let mut seen = HashSet :: new ( ) ;
517548 loaded. retain ( |img| seen. insert ( img. source . clone ( ) ) ) ;
518549
519- if reload {
550+ // Always delete existing Function packages whose source matches the render
551+ // images we are about to push. Without this, Crossplane's dependency
552+ // resolution keeps the old Function with a stale digest from the published
553+ // version, causing ImagePullBackOff when the local registry has a different
554+ // digest. With --reload we also delete ConfigurationRevisions.
555+ {
520556 let function_sources: HashSet < String > = loaded
521557 . iter ( )
522558 . filter ( |img| !is_configuration_image ( & img. source ) )
@@ -534,7 +570,7 @@ fn run_local_path(
534570 ) ?;
535571 if removed_functions > 0 || removed_function_revisions > 0 {
536572 log:: info!(
537- "Reload requested; deleted {} Function package(s) and {} FunctionRevision(s) from matching sources before re-apply" ,
573+ "Deleted {} Function package(s) and {} FunctionRevision(s) from matching sources before re-apply" ,
538574 removed_functions,
539575 removed_function_revisions
540576 ) ;
@@ -639,8 +675,9 @@ spec:
639675 // dependencies (skipDependencyResolution is intentionally not set).
640676 for pull_ref in & config_pull_refs {
641677 let ( img_path, _) = split_ref ( pull_ref) ;
642- let name = img_path. rsplit ( '/' ) . next ( ) . unwrap_or ( img_path) ;
643- apply_configuration ( name, pull_ref, skip_dependency_resolution, reload) ?;
678+ let path = strip_registry ( img_path) ;
679+ let name = path. replace ( '/' , "-" ) ;
680+ apply_configuration ( & name, pull_ref, skip_dependency_resolution, reload) ?;
644681 }
645682
646683 Ok ( ( ) )
@@ -1051,15 +1088,53 @@ fn build_patched_configuration_image(
10511088 unique_suffix( )
10521089 ) ) ;
10531090 fs:: create_dir_all ( & build_dir) ?;
1054- fs:: write ( build_dir. join ( "package.yaml" ) , package_yaml) ?;
1091+
1092+ // Extract the source image's filesystem via docker create + export,
1093+ // avoiding multi-stage FROM which breaks when Docker's snapshot cache
1094+ // is stale for images loaded via `docker load`.
1095+ let container_name = format ! ( "hops-extract-{}" , unique_suffix( ) ) ;
1096+ let create_out = Command :: new ( "docker" )
1097+ . args ( [ "create" , "--name" , & container_name, source_image, "true" ] )
1098+ . output ( ) ?;
1099+ if !create_out. status . success ( ) {
1100+ return Err ( format ! (
1101+ "docker create failed: {}" ,
1102+ String :: from_utf8_lossy( & create_out. stderr)
1103+ )
1104+ . into ( ) ) ;
1105+ }
1106+
1107+ let content_dir = build_dir. join ( "content" ) ;
1108+ fs:: create_dir_all ( & content_dir) ?;
1109+
1110+ let export_status = Command :: new ( "sh" )
1111+ . args ( [
1112+ "-c" ,
1113+ & format ! (
1114+ "docker export {} | tar -xf - -C {}" ,
1115+ container_name,
1116+ content_dir. to_string_lossy( )
1117+ ) ,
1118+ ] )
1119+ . status ( ) ?;
1120+
1121+ // Always remove the temp container.
1122+ let _ = Command :: new ( "docker" )
1123+ . args ( [ "rm" , "-f" , & container_name] )
1124+ . output ( ) ;
1125+
1126+ if !export_status. success ( ) {
1127+ let _ = fs:: remove_dir_all ( & build_dir) ;
1128+ return Err ( "docker export failed" . into ( ) ) ;
1129+ }
1130+
1131+ // Replace package.yaml with the patched version.
1132+ fs:: write ( content_dir. join ( "package.yaml" ) , package_yaml) ?;
1133+
1134+ // Build from scratch using the extracted + patched content.
10551135 fs:: write (
10561136 build_dir. join ( "Dockerfile" ) ,
1057- format ! (
1058- "FROM {source_image} AS src\n \
1059- FROM scratch\n \
1060- COPY --from=src / /\n \
1061- COPY package.yaml /package.yaml\n "
1062- ) ,
1137+ "FROM scratch\n COPY content/ /\n " ,
10631138 ) ?;
10641139
10651140 let target_tag = format ! (
@@ -1220,6 +1295,47 @@ fn docker_build_from(src: &str, tag: &str) -> Result<(), Box<dyn Error>> {
12201295 Ok ( ( ) )
12211296}
12221297
1298+ /// Delete inactive ConfigurationRevisions whose package points at the local
1299+ /// registry. These are left over from `config install --path` and can block
1300+ /// dependency resolution when switching to a published version.
1301+ fn delete_local_registry_config_revisions ( config_name : & str ) -> Result < ( ) , Box < dyn Error > > {
1302+ let output = run_cmd_output (
1303+ "kubectl" ,
1304+ & [
1305+ "get" ,
1306+ "configurationrevision.pkg.crossplane.io" ,
1307+ "-o" ,
1308+ "jsonpath={range .items[*]}{.metadata.name}|{.spec.package}|{.spec.desiredState}\\ n{end}" ,
1309+ ] ,
1310+ ) ?;
1311+
1312+ for line in output. lines ( ) {
1313+ let parts: Vec < & str > = line. split ( '|' ) . collect ( ) ;
1314+ if parts. len ( ) < 3 {
1315+ continue ;
1316+ }
1317+ let rev_name = parts[ 0 ] . trim ( ) ;
1318+ let package = parts[ 1 ] . trim ( ) ;
1319+ let state = parts[ 2 ] . trim ( ) ;
1320+
1321+ if !rev_name. starts_with ( config_name) {
1322+ continue ;
1323+ }
1324+ if package. contains ( REGISTRY_PULL ) && state == "Inactive" {
1325+ run_cmd (
1326+ "kubectl" ,
1327+ & [
1328+ "delete" ,
1329+ "configurationrevision.pkg.crossplane.io" ,
1330+ rev_name,
1331+ ] ,
1332+ ) ?;
1333+ log:: info!( "Deleted stale local ConfigurationRevision '{}'" , rev_name) ;
1334+ }
1335+ }
1336+ Ok ( ( ) )
1337+ }
1338+
12231339#[ cfg( test) ]
12241340mod tests {
12251341 use super :: * ;
0 commit comments