diff --git a/src/pipeline/pass_docker.c b/src/pipeline/pass_docker.c index b845211a4..f5f303866 100644 --- a/src/pipeline/pass_docker.c +++ b/src/pipeline/pass_docker.c @@ -11,9 +11,10 @@ * ENV); a reference without a value leaves the image unknown, and an unknown * image is no edge. * - * Image identity is the normalised repository: docker.io for an image - * without a registry host, library/ for an official one-segment image; - * tag and digest stay on the edge. + * Image identity is the repository as written, tag and digest dropped (they + * stay on the edge). Its registry is the host the reference names -- Docker + * Hub's spellings as docker.io, with library/ for an official one-segment + * image -- else "unknown": no host is invented for a reference naming none. * * PHASE 4 P4.1b: what the container runs. The last stage's ENTRYPOINT (else * CMD) SPAWNS -- from the Dockerfile's Module {api:dockerfile}: @@ -268,7 +269,6 @@ static const char *dk_expand(dk_t *d, const dk_var_t *vars, const char *text) { return out; } -/* The repository identity of image reference `ref` into out. */ /* Only characters an image reference can hold (name, :tag, @digest) -- a * template's placeholder (`<.Values.image>`, ``) or an expression is * no image. */ @@ -281,7 +281,15 @@ static bool dk_ref_chars(const char *ref) { return ref[0] != '\0'; } -static bool dk_normalize(const char *ref, char *out, size_t sz) { +/* The registry of a reference that names no host. */ +static const char DK_REGISTRY_UNKNOWN[] = "unknown"; + +/* The repository identity of image reference `ref` into out, the registry + * its host names into reg. A reference without a host is the repository as + * written, its registry unknown: docker.io is only Docker's default -- a + * short-name search list (Podman, CRI-O) or a local-only build tag resolves + * elsewhere or nowhere. */ +static bool dk_normalize(const char *ref, char *out, size_t sz, char *reg, size_t reg_sz) { if (!dk_ref_chars(ref)) { return false; } @@ -300,15 +308,12 @@ static bool dk_normalize(const char *ref, char *out, size_t sz) { return false; } char *first_end = strchr(r, '/'); - if (!first_end) { - snprintf(out, sz, "docker.io/library/%s", r); - return true; - } - size_t fl = (size_t)(first_end - r); - bool host = memchr(r, '.', fl) || memchr(r, ':', fl) || - (fl == strlen("localhost") && strncmp(r, "localhost", fl) == 0); + size_t fl = first_end ? (size_t)(first_end - r) : 0; + bool host = first_end && (memchr(r, '.', fl) || memchr(r, ':', fl) || + (fl == strlen("localhost") && strncmp(r, "localhost", fl) == 0)); if (!host) { - snprintf(out, sz, "docker.io/%s", r); + snprintf(out, sz, "%s", r); + snprintf(reg, reg_sz, "%s", DK_REGISTRY_UNKNOWN); return true; } const char *rest = first_end + SKIP_ONE; @@ -318,8 +323,10 @@ static bool dk_normalize(const char *ref, char *out, size_t sz) { (fl == strlen("registry-1.docker.io") && strncmp(r, "registry-1.docker.io", fl) == 0); if (hub) { snprintf(out, sz, "docker.io/%s%s", strchr(rest, '/') ? "" : "library/", rest); + snprintf(reg, reg_sz, "docker.io"); } else { snprintf(out, sz, "%.*s/%s", (int)fl, r, rest); + snprintf(reg, reg_sz, "%.*s", (int)fl, r); } return true; } @@ -365,15 +372,20 @@ static int dk_stage_of(const dk_t *d, const char *ref) { int64_t cbm_pipeline_image_node(cbm_pipeline_ctx_t *ctx, const char *ref) { char repo[CBM_SZ_512]; - if (!ref || dk_ieq(ref, "scratch") || !dk_normalize(ref, repo, sizeof(repo))) { + char reg[CBM_SZ_512]; + if (!ref || dk_ieq(ref, "scratch") || + !dk_normalize(ref, repo, sizeof(repo), reg, sizeof(reg))) { return 0; } char qn[CBM_SZ_1K]; snprintf(qn, sizeof(qn), "__image__%s", repo); char esc_repo[CBM_SZ_1K]; + char esc_reg[CBM_SZ_1K]; cbm_json_escape(esc_repo, (int)sizeof(esc_repo), repo); + cbm_json_escape(esc_reg, (int)sizeof(esc_reg), reg); char node_props[CBM_SZ_2K]; - snprintf(node_props, sizeof(node_props), "{\"repository\":\"%s\"}", esc_repo); + snprintf(node_props, sizeof(node_props), "{\"repository\":\"%s\",\"registry\":\"%s\"}", + esc_repo, esc_reg); return cbm_gbuf_upsert_node(ctx->gbuf, "Image", repo, qn, "", 0, 0, node_props); } @@ -2054,7 +2066,7 @@ static int dk_hub_match(const char *tag, const char *const *comps, int nc) { } /* The Image of a component: `:` (`*-frontend`: the - * prefix's version masked) {repository, component, tag_pattern}. */ + * prefix's version masked) {repository, registry, component, tag_pattern}. */ static int64_t dk_hub_image(cbm_pipeline_ctx_t *ctx, const char *repo, const char *comp, char pre_sep, char suf_sep) { char head[PAIR_LEN + SKIP_ONE] = {0}; @@ -2073,16 +2085,24 @@ static int64_t dk_hub_image(cbm_pipeline_ctx_t *ctx, const char *repo, const cha snprintf(pattern, sizeof(pattern), "%s%s%s", head, comp, tail); snprintf(name, sizeof(name), "%s:%s", repo, pattern); snprintf(qn, sizeof(qn), "__image__%s", name); + char same[CBM_SZ_1K]; + char reg[CBM_SZ_512]; + if (!dk_normalize(repo, same, sizeof(same), reg, sizeof(reg))) { + snprintf(reg, sizeof(reg), "%s", DK_REGISTRY_UNKNOWN); + } char esc_r[CBM_SZ_1K]; + char esc_g[CBM_SZ_1K]; char esc_c[CBM_SZ_1K]; char esc_p[CBM_SZ_1K]; cbm_json_escape(esc_r, (int)sizeof(esc_r), repo); + cbm_json_escape(esc_g, (int)sizeof(esc_g), reg); cbm_json_escape(esc_c, (int)sizeof(esc_c), comp); cbm_json_escape(esc_p, (int)sizeof(esc_p), pattern); char props[CBM_SZ_4K]; snprintf(props, sizeof(props), - "{\"repository\":\"%s\",\"component\":\"%s\",\"tag_pattern\":\"%s\"}", esc_r, esc_c, - esc_p); + "{\"repository\":\"%s\",\"registry\":\"%s\",\"component\":\"%s\"," + "\"tag_pattern\":\"%s\"}", + esc_r, esc_g, esc_c, esc_p); return cbm_gbuf_upsert_node(ctx->gbuf, "Image", name, qn, "", 0, 0, props); } diff --git a/src/pipeline/pipeline_internal.h b/src/pipeline/pipeline_internal.h index a54dea245..5efc1248c 100644 --- a/src/pipeline/pipeline_internal.h +++ b/src/pipeline/pipeline_internal.h @@ -1332,9 +1332,10 @@ bool cbm_ansible_is_sequence(const char *src); * (pass_terraform.c). */ bool cbm_repo_rel_join(const char *dir, const char *v, char *out, size_t sz); -/* The Image node of image reference `ref` (its normalised repository: - * docker.io for no registry host, library/ for an official image; tag and - * digest dropped), made when new; 0 for scratch or no image. */ +/* The Image node of image reference `ref` (its repository as written, tag + * and digest dropped; Docker Hub's host spellings as docker.io with library/ + * for an official image; registry "unknown" when the reference names no + * host -- none is invented), made when new; 0 for scratch or no image. */ int64_t cbm_pipeline_image_node(cbm_pipeline_ctx_t *ctx, const char *ref); /* A chart repository URL as an identity (pass_k8s.c): `url` trimmed, its diff --git a/src/store/store.h b/src/store/store.h index 123e7645e..803df81d8 100644 --- a/src/store/store.h +++ b/src/store/store.h @@ -29,8 +29,11 @@ typedef struct cbm_store_read_scope cbm_store_read_scope_t; * 1 #1108: File-node QNs keep the file extension. * 2 Kubernetes objects: one Resource per YAML document, QN * ::///; a - * Dockerfile. keeps its suffix in its QNs. */ -#define CBM_INDEX_FORMAT_VERSION 2 + * Dockerfile. keeps its suffix in its QNs. + * 3 Image identity invents no registry: a reference without a host is + * __image__, registry "unknown" -- no longer + * __image__docker.io/[library/]. */ +#define CBM_INDEX_FORMAT_VERSION 3 #define CBM_STORE_CANCELLED (-3) #define CBM_STORE_SCAN_LIMIT (-4) #define CBM_STORE_CALLBACK_ERR (-5) diff --git a/tests/test_grammar_probe_f.c b/tests/test_grammar_probe_f.c index 6450ab5b5..454d7b35f 100644 --- a/tests/test_grammar_probe_f.c +++ b/tests/test_grammar_probe_f.c @@ -1704,7 +1704,7 @@ TEST(probe_mcp_trace_run) { ASSERT_NOT_NULL(r); /* qualified names are grouped by prefix, split at a dot */ ASSERT_NOT_NULL(strstr(r, "Deployment/-/server")); - ASSERT_NOT_NULL(strstr(r, "\"__image__docker\",\"rows\":[[\"io/acme/server\"")); + ASSERT_NOT_NULL(strstr(r, "[\"__image__acme/server\"")); /* dotless: the whole QN */ ASSERT_NOT_NULL(strstr(r, ".svc\",\"rows\":[[\"Dockerfile\"")); /* its Module */ ASSERT_NOT_NULL(strstr(r, ".cmd.server\",\"rows\":[[\"main\"")); /* what it starts */ ASSERT_NOT_NULL(strstr(r, "Deployment/-/server#env/server/LISTEN_PORT")); @@ -1961,8 +1961,8 @@ static char *gpf_images(const GpfFile *files, int nfiles) { } while (0) /* A Dockerfile DEPENDS_ON each image it builds on: FROM and COPY --from, - * with ARG defaults expanded, under the image's repository (docker.io and - * library/ filled in, a registry host kept, tag and digest dropped). A stage + * with ARG defaults expanded, under the image's repository (as written: a + * registry host kept, none invented; tag and digest dropped). A stage * (by name or index), scratch and an ARG without a value are no image. */ TEST(probe_dockerfile_bases_are_images) { static const GpfFile files[] = { @@ -1979,12 +1979,72 @@ TEST(probe_dockerfile_bases_are_images) { "COPY --from=registry.example.com:5000/team/tools:2 /bin/x /x\n"}}; GPF_ASSERT_IMAGES(files, 1, "@svc/Dockerfile -> docker.io/bitnami/kubectl (copy-from)\n" - "@svc/Dockerfile -> docker.io/library/golang (from)\n" "@svc/Dockerfile -> gcr.io/distroless/static (from)\n" + "@svc/Dockerfile -> golang (from)\n" "@svc/Dockerfile -> registry.example.com:5000/team/tools (copy-from)"); PASS(); } +/* Every Image node as " []", sorted, one per line. */ +static char *gpf_image_nodes(const GpfFile *files, int nfiles) { + GpfProj lp; + cbm_store_t *store = gpf_index_files(&lp, files, nfiles); + char *out = NULL; + if (store) { + sqlite3_stmt *st = NULL; + const char *sql = "SELECT coalesce(group_concat(x, char(10)), '') FROM (SELECT name || ' [' " + "|| coalesce(json_extract(properties,'$.registry'), '-') || ']' AS x " + "FROM nodes WHERE project=?1 AND label='Image' ORDER BY x)"; + if (sqlite3_prepare_v2(cbm_store_get_db(store), sql, -1, &st, NULL) == SQLITE_OK) { + sqlite3_bind_text(st, 1, lp.project, -1, SQLITE_STATIC); + if (sqlite3_step(st) == SQLITE_ROW) { + out = strdup((const char *)sqlite3_column_text(st, 0)); + } + } + sqlite3_finalize(st); + } + gpf_cleanup(&lp, store); + return out; +} + +/* An image reference that names no registry host keeps it unknown: the Image + * is the repository as written, registry "unknown" -- never docker.io, which + * is only Docker's default (a short-name search list or a local-only build + * tag resolves elsewhere or nowhere). A stated host is kept, its Docker Hub + * spellings as docker.io with library/ for an official one-segment name. + * Every source alike: FROM, a K8s container, a compose service, a build tag. */ +TEST(probe_image_without_registry_host_stays_unknown) { + static const GpfFile files[] = { + {"svc/Dockerfile", "FROM nginx:1.27\nFROM acme/base:2 AS b\nFROM docker.io/redis:7 AS c\n" + "FROM index.docker.io/acme/tool AS d\n" + "FROM ghcr.io/acme/app@sha256:0123 AS e\n" + "FROM localhost:5000/team/x AS f\n"}, + {"app.yaml", "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: web\nspec:\n" + " template:\n spec:\n containers:\n - name: web\n" + " image: busybox:1.36\n - name: side\n" + " image: quay.io/acme/sidecar:1\n"}, + {"compose.yaml", "services:\n web:\n image: acme/web:1\n"}, + {"build.sh", "#!/bin/sh\ncd \"$(dirname \"$0\")\"\ndocker build -t myapp:dev svc\n"}}; + char *got = gpf_image_nodes(files, 4); + static const char want[] = "acme/base [unknown]\n" + "acme/web [unknown]\n" + "busybox [unknown]\n" + "docker.io/acme/tool [docker.io]\n" + "docker.io/library/redis [docker.io]\n" + "ghcr.io/acme/app [ghcr.io]\n" + "localhost:5000/team/x [localhost:5000]\n" + "myapp [unknown]\n" + "nginx [unknown]\n" + "quay.io/acme/sidecar [quay.io]"; + if (!got || strcmp(got, want) != 0) { + fprintf(stderr, "image nodes got:\n%s\nwant:\n%s\n", got ? got : "(null)", want); + free(got); + FAIL("Image nodes and registries"); + } + free(got); + PASS(); +} + /* A build file named for its variant is a Dockerfile of its own (its own * Module, beside the plain Dockerfile): Dockerfile.debug, Dockerfile-test, * Containerfile, pipeline.Dockerfile. A template of one (Dockerfile.j2) and a @@ -2000,10 +2060,10 @@ TEST(probe_dockerfile_variants_are_dockerfiles) { {"tools/Dockerfile.j2", "FROM {{ base_image }}\n"}}; GPF_ASSERT_IMAGES(files, 7, "@Containerfile -> quay.io/podman/stable (from)\n" - "@svc/Dockerfile -> docker.io/library/alpine (from)\n" - "@svc/Dockerfile-test -> docker.io/library/ubuntu (from)\n" - "@svc/Dockerfile.debug -> docker.io/library/debian (from)\n" - "@tools/pipeline.Dockerfile -> docker.io/library/node (from)"); + "@svc/Dockerfile -> alpine (from)\n" + "@svc/Dockerfile-test -> ubuntu (from)\n" + "@svc/Dockerfile.debug -> debian (from)\n" + "@tools/pipeline.Dockerfile -> node (from)"); PASS(); } @@ -2083,8 +2143,8 @@ TEST(probe_k8s_container_images_are_images) { " image: \n"}}; char *got = gpf_image_users(files, 1); static const char want[] = "CronJob/nightly -> ghcr.io/acme/job\n" - "Deployment/web -> docker.io/library/busybox\n" - "Deployment/web -> docker.io/library/nginx\n" + "Deployment/web -> busybox\n" + "Deployment/web -> nginx\n" "Widget/w -> quay.io/acme/widget"; if (!got || strcmp(got, want) != 0) { fprintf(stderr, "image users got:\n%s\nwant:\n%s\n", got ? got : "(null)", want); @@ -2145,10 +2205,10 @@ TEST(probe_build_configs_build_images_from_dockerfiles) { " missing:\n image: acme/missing\n build: ./nope\n" " unknown:\n image: ${NOPE}/x\n build: ./src/web\n"}}; char *got = gpf_builds(files, 5); - static const char want[] = "docker.io/library/api -> src/api/Dockerfile.prod (skaffold)\n" - "docker.io/library/dev-web -> src/web/Dockerfile (skaffold)\n" - "docker.io/library/web -> src/web/Dockerfile (skaffold)\n" - "ghcr.io/acme/frontend -> src/web/Dockerfile (compose)"; + static const char want[] = "api -> src/api/Dockerfile.prod (skaffold)\n" + "dev-web -> src/web/Dockerfile (skaffold)\n" + "ghcr.io/acme/frontend -> src/web/Dockerfile (compose)\n" + "web -> src/web/Dockerfile (skaffold)"; if (!got || strcmp(got, want) != 0) { fprintf(stderr, "builds got:\n%s\nwant:\n%s\n", got ? got : "(null)", want); free(got); @@ -2203,10 +2263,10 @@ TEST(probe_compose_services_are_resources) { " worker:\n build:\n context: .\n dockerfile: worker/Dockerfile\n"}}; char *got = gpf_compose(files, 3); static const char want[] = - "Service/cache -> Image:docker.io/library/redis (USAGE image)\n" + "Service/cache -> Image:redis (USAGE image)\n" "Service/cache -> Service/db (DEPENDS_ON depends_on)\n" - "Service/db -> Image:docker.io/library/postgres (USAGE image)\n" - "Service/web -> Image:docker.io/acme/web (USAGE image)\n" + "Service/db -> Image:postgres (USAGE image)\n" + "Service/web -> Image:acme/web (USAGE image)\n" "Service/web -> Module:web/Dockerfile (DEPENDS_ON build)\n" "Service/web -> Service/cache (DEPENDS_ON depends_on)\n" "Service/web -> Service/db (DEPENDS_ON depends_on)\n" @@ -2314,17 +2374,17 @@ TEST(probe_image_hub_splits_by_component) { " image: acme/solo:2\n"}}; enum { NF = sizeof(files) / sizeof(files[0]) }; char *got = gpf_builds(files, NF); - static const char want_builds[] = "docker.io/acme/demo:*-a -> svc/a/Dockerfile (compose)\n" - "docker.io/acme/demo:*-b -> svc/b/Dockerfile (compose)\n" - "docker.io/acme/demo:*-c -> svc/c/Dockerfile (compose)\n" - "docker.io/acme/solo -> solo/Dockerfile (compose)\n" - "docker.io/acme/tool -> tool/Dockerfile (compose)\n" - "docker.io/acme/tool:test -> tool/Dockerfile.test (compose)\n" - "docker.io/acme/x -> w1/Dockerfile (compose)\n" - "docker.io/acme/x -> w2/Dockerfile.alt (compose)\n" - "docker.io/acme/x:*-v -> v/Dockerfile (compose)\n" - "docker.io/acme/y:*-p -> p/Dockerfile (compose)\n" - "docker.io/acme/y:*-q -> q/Dockerfile (compose)"; + static const char want_builds[] = "acme/demo:*-a -> svc/a/Dockerfile (compose)\n" + "acme/demo:*-b -> svc/b/Dockerfile (compose)\n" + "acme/demo:*-c -> svc/c/Dockerfile (compose)\n" + "acme/solo -> solo/Dockerfile (compose)\n" + "acme/tool -> tool/Dockerfile (compose)\n" + "acme/tool:test -> tool/Dockerfile.test (compose)\n" + "acme/x -> w1/Dockerfile (compose)\n" + "acme/x -> w2/Dockerfile.alt (compose)\n" + "acme/x:*-v -> v/Dockerfile (compose)\n" + "acme/y:*-p -> p/Dockerfile (compose)\n" + "acme/y:*-q -> q/Dockerfile (compose)"; if (!got || strcmp(got, want_builds) != 0) { fprintf(stderr, "builds got:\n%s\nwant:\n%s\n", got ? got : "(null)", want_builds); free(got); @@ -2332,20 +2392,20 @@ TEST(probe_image_hub_splits_by_component) { } free(got); got = gpf_image_users(files, NF); - static const char want_users[] = "Deployment/b -> docker.io/acme/demo:*-b\n" - "Deployment/s -> docker.io/acme/solo\n" - "Deployment/z -> docker.io/acme/demo\n" - "Service/a -> docker.io/acme/demo:*-a\n" - "Service/b -> docker.io/acme/demo:*-b\n" - "Service/c -> docker.io/acme/demo:*-c\n" - "Service/p -> docker.io/acme/y:*-p\n" - "Service/q -> docker.io/acme/y:*-q\n" - "Service/solo -> docker.io/acme/solo\n" - "Service/tool -> docker.io/acme/tool\n" - "Service/tooltest -> docker.io/acme/tool:test\n" - "Service/v -> docker.io/acme/x:*-v\n" - "Service/w1 -> docker.io/acme/x\n" - "Service/w2 -> docker.io/acme/x"; + static const char want_users[] = "Deployment/b -> acme/demo:*-b\n" + "Deployment/s -> acme/solo\n" + "Deployment/z -> acme/demo\n" + "Service/a -> acme/demo:*-a\n" + "Service/b -> acme/demo:*-b\n" + "Service/c -> acme/demo:*-c\n" + "Service/p -> acme/y:*-p\n" + "Service/q -> acme/y:*-q\n" + "Service/solo -> acme/solo\n" + "Service/tool -> acme/tool\n" + "Service/tooltest -> acme/tool:test\n" + "Service/v -> acme/x:*-v\n" + "Service/w1 -> acme/x\n" + "Service/w2 -> acme/x"; if (!got || strcmp(got, want_users) != 0) { fprintf(stderr, "image users got:\n%s\nwant:\n%s\n", got ? got : "(null)", want_users); free(got); @@ -6143,6 +6203,7 @@ SUITE(grammar_probe_f) { RUN_TEST(probe_mcp_trace_run); RUN_TEST(probe_mcp_trace_deploy); RUN_TEST(probe_dockerfile_bases_are_images); + RUN_TEST(probe_image_without_registry_host_stays_unknown); RUN_TEST(probe_dockerfile_variants_are_dockerfiles); RUN_TEST(probe_dockerfile_entrypoint_runs); RUN_TEST(probe_k8s_container_images_are_images); diff --git a/tests/test_index_format.c b/tests/test_index_format.c index ceec7b370..dde173ce5 100644 --- a/tests/test_index_format.c +++ b/tests/test_index_format.c @@ -16,6 +16,7 @@ #include "test_framework.h" #include "repro_harness.h" /* RProj, rh_index_files, rh_count_label, rh_cleanup */ #include "foundation/log.h" +#include #include #include #include @@ -243,7 +244,60 @@ TEST(index_format_legacy_index_rebuilds_and_repairs) { PASS(); } +/* ── Test 3: an index from before format 3 drops its invented hosts ─── */ + +/* Format 2 stored an image without a registry host under an invented + * docker.io identity. Refreshed in place, only touched files would get the + * new identity: both forms of one image in a single graph. */ +static const RFile k_image_files[] = { + {"a/Dockerfile", "FROM nginx:1.27\n"}, + {"k8s.yaml", "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: web\nspec:\n" + " template:\n spec:\n containers:\n - name: web\n" + " image: nginx:1.27\n"}, +}; + +TEST(index_format_v2_images_rebuild_without_invented_host) { + RProj lp; + cbm_store_t *q0 = rh_index_files(&lp, k_image_files, 2); + ASSERT_NOT_NULL(q0); + cbm_store_close(q0); + + /* The format-2 shape: the same node, edges kept, under its old identity. */ + cbm_store_t *w = cbm_store_open_path(lp.dbpath); + ASSERT_NOT_NULL(w); + ASSERT_EQ(sqlite3_exec(cbm_store_get_db(w), + "UPDATE nodes SET name='docker.io/library/nginx', " + "qualified_name='__image__docker.io/library/nginx', " + "properties='{\"repository\":\"docker.io/library/nginx\"}' " + "WHERE label='Image' AND name='nginx'", + NULL, NULL, NULL), + SQLITE_OK); + ASSERT_EQ(sqlite3_changes(cbm_store_get_db(w)), 1); + ASSERT_EQ(cbm_store_set_format_version(w, 2), CBM_STORE_OK); + cbm_store_close(w); + + char *resp = index_capture(&lp); + ASSERT_NOT_NULL(resp); + free(resp); + if (!strstr(g_log_buf, "format_change_reindex")) { + FAIL("a format-2 index was not routed through the full-reindex path"); + } + cbm_store_t *r = cbm_store_open_path(lp.dbpath); + ASSERT_NOT_NULL(r); + cbm_node_t n = {0}; + ASSERT_EQ(cbm_store_find_node_by_qn(r, lp.project, "__image__docker.io/library/nginx", &n), + CBM_STORE_NOT_FOUND); + cbm_node_free_fields(&n); + ASSERT_EQ(cbm_store_find_node_by_qn(r, lp.project, "__image__nginx", &n), CBM_STORE_OK); + ASSERT_NOT_NULL(n.properties_json); + ASSERT_NOT_NULL(strstr(n.properties_json, "\"registry\":\"unknown\"")); + cbm_node_free_fields(&n); + rh_cleanup(&lp, r); + PASS(); +} + SUITE(index_format) { RUN_TEST(index_format_siblings_distinct_and_searchable); RUN_TEST(index_format_legacy_index_rebuilds_and_repairs); + RUN_TEST(index_format_v2_images_rebuild_without_invented_host); } diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 180578cf8..b40750e51 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -3980,10 +3980,10 @@ static char *incremental_compose_edit(bool legacy, cbm_incremental_route_t *rout char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/composeedit.db", tmp); static const char old_img[] = "SELECT COUNT(*) FROM nodes WHERE label='Image' AND " - "name='docker.io/acme/web-old'"; + "name='acme/web-old'"; static const char new_edge[] = "SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id WHERE e.type='DEPENDS_ON' " - "AND s.name='docker.io/acme/web-new'"; + "AND s.name='acme/web-new'"; if (k8s_inc_index(tmp, db_path) != 0 || k8s_inc_count(db_path, old_img) != 1) { th_rmtree(tmp); return NULL; @@ -4131,13 +4131,13 @@ static char *incremental_image_split(bool rename, cbm_incremental_route_t *route static const char to_repo[] = "SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id JOIN nodes t ON " "t.id=e.target_id WHERE e.type='USAGE' AND s.name='Service/runner' AND " - "t.name='docker.io/acme/demo'"; + "t.name='acme/demo'"; static const char to_b[] = "SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id JOIN nodes t ON " "t.id=e.target_id WHERE e.type='USAGE' AND s.name='Service/runner' AND " - "t.name='docker.io/acme/demo:*-b'"; + "t.name='acme/demo:*-b'"; static const char b_img[] = - "SELECT COUNT(*) FROM nodes WHERE label='Image' AND name='docker.io/acme/demo:*-b'"; + "SELECT COUNT(*) FROM nodes WHERE label='Image' AND name='acme/demo:*-b'"; if (k8s_inc_index(tmp, db_path) != 0 || k8s_inc_count(db_path, to_b) != 1) { th_rmtree(tmp); return NULL;