mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-25 06:33:13 +08:00
Merge pull request #2654 from DeusData/fix/image-unknown-registry
fix(docker): never invent a registry host for an image
This commit is contained in:
+38
-18
@@ -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>`, `<tpl>`) 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: `<repo>:<pattern>` (`*-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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+5
-2
@@ -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
|
||||
* <file>::<group>/<Kind>/<declared namespace or ->/<name>; a
|
||||
* Dockerfile.<variant> keeps its suffix in its QNs. */
|
||||
#define CBM_INDEX_FORMAT_VERSION 2
|
||||
* Dockerfile.<variant> keeps its suffix in its QNs.
|
||||
* 3 Image identity invents no registry: a reference without a host is
|
||||
* __image__<repository as written>, registry "unknown" -- no longer
|
||||
* __image__docker.io/[library/]<repository>. */
|
||||
#define CBM_INDEX_FORMAT_VERSION 3
|
||||
#define CBM_STORE_CANCELLED (-3)
|
||||
#define CBM_STORE_SCAN_LIMIT (-4)
|
||||
#define CBM_STORE_CALLBACK_ERR (-5)
|
||||
|
||||
+103
-42
@@ -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 "<name> [<registry>]", 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: <image-built-from-dockerfile>\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);
|
||||
|
||||
@@ -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 <sqlite3.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user