Is your feature request related to a problem? Please describe.
Last week I sent score-compose#496 to fix a path-traversal in containers[].files[].source. Before opening the PR I checked whether the same pattern existed elsewhere in the org. It does. score-go/loader/normalize.go:54 has the same vulnerable readFile helper, and score-k8s/internal/convert/container_files.go:66 has the same code in line-for-line form. So one bug, three independent fixes.
That got me curious about how much of the code overlaps between the two active reference implementations, so I ran a per-file diff over score-compose/internal and score-k8s/internal. The numbers were higher than I expected. Out of 14 files that have a counterpart in both repos, five are 90% identical or better. logging/logging.go is byte-identical. The orchestration loop at provisioners/core.go is 385 lines and 76% identical, with most of the divergence being which target-specific types it composes against.
So the path traversal living in three places isn't an accident. It's a symptom of the fact that score-compose and score-k8s carry roughly 2,700 lines of nearly-the-same Go in their internal/ trees, evolving independently. score-compose#489 (the work to make envprov a loadable provisioner type) is the second symptom I tripped over: it's a useful feature that score-k8s users currently can't get because the change hasn't been mirrored.
This isn't blame. The maintainers across all three repos are the same four people; the drift exists despite attention, not because of inattention. It's structural.
Describe the solution you'd like
Lift the parts of internal/provisioners and internal/command that are already nearly identical into score-go, so both consumers depend on one canonical version instead of two evolving copies.
The three things I'd start with, in order:
The Provisioner interface and the ProvisionResources orchestrator. Currently provisioners/core.go in both repos. About 385 lines, 76% identical. It's already generic over framework.State[Extras] which lives in score-go, so the actual code move is mostly mechanical. The non-trivial part is that score-compose's version takes a *compose.Project as its last parameter and score-k8s's takes a namespace string. I think the cleanest thing is to push both target-specific args into the existing State[Extras].Extras field, which was designed for this exact case, but that's worth bikeshedding an interface like ProvisionTarget with two concrete implementations would also work.
cmdprov and templateprov. cmdprov is the easier one: 197 lines, 96% identical. The only real divergence is a 9-line <mode> arg substitution score-k8s added at lines 152-160 of its copy. That can be a single Options.ArgReplacements map[string]string field on the provisioner, nil in score-compose, populated with {"<mode>": "provision"} in score-k8s. templateprov is harder: 290 lines, 72% identical, and the remaining 28% is genuinely target-specific (compose vs k8s template outputs). So templateprov is an extract-the-shared-bits exercise rather than a full move.
The .provisioners.yaml loader. provisioners/loader/load.go, 148 lines, 95% identical, with no behavior that's actually implementation-specific. This is the simplest of the three and could land as one PR.
After all three, each consumer drops somewhere around 2,000 lines from its internal/ tree. Any future Score implementation (score-aca and score-radius are in progress; third-party forks may follow) inherits the orchestration plus the built-in provisioners for free instead of copying them.
What I don't want this to do: pull target-specific render code into score-go. score-compose/internal/compose/convert.go and score-k8s/internal/convert/ stay where they are. The orchestration framework moves; the docker-compose-vs-k8s rendering doesn't.
How I'd want to roll this out: as one PR against score-go for Phase 1, get it merged, bump both downstream repos to use it (with adapter shims in their existing internal/provisioners/core.go for one release cycle so there's no hard cutover), and revisit Phase 2 based on how that landed. I don't want to commit to weeks of work in writing. I have the context from #496 and from the score-k8s mirror PR I'm sending shortly, and I'd rather not write the same patch a fourth time when the next file-handling bug shows up.
Describe alternatives you've considered
The two alternatives I actually weighed.
Keep the status quo and rely on cross-repo PR habits. This is the current model. Whoever ships a change in score-compose remembers to mirror it in score-k8s, and vice versa. It hasn't held up. The two examples I cited are the ones I personally tripped over in a two-week window; older drift cases probably exist that I didn't go looking for. The missing ingredient isn't maintainer attention. It's a single canonical home for the shared code.
A cross-implementation conformance test suite. A separate repo with canonical Score files plus expected behaviors that each implementation runs in CI. This detects drift after it happens, which has value, but it doesn't prevent drift, and it adds a whole new maintained artifact (the kit, the runners, an expectation DSL). I think a conformance kit is worth doing eventually, just not first if the shared code is unified, the kit's job gets considerably smaller because it only has to test the bits that legitimately differ.
There's also a "leave it alone because score-go is intentionally narrow" position that's a real one. If the maintainers' view is that score-go is intentionally scoped to parsing, schema, and framework primitives, and that provisioner machinery is intentionally an implementation-level concern, then duplication is the price of that separation, and that's defensible. I don't have a strong opinion on which side is right. That's the question I'd want answered before writing any code.
Additional context
The per-file similarity numbers from running diff over both internal/ trees (commands at the bottom of this issue if you want to reproduce):
File (under internal/) |
Lines |
Diff |
% identical |
logging/logging.go |
53 |
0 |
100% |
command/version.go |
139 |
2 |
99% |
provisioners/cmdprov/commandprov.go |
197 |
15 |
96% |
provisioners/loader/load.go |
148 |
14 |
95% |
command/provisioners.go |
124 |
13 |
95% |
command/resources.go |
198 |
38 |
90% |
version/version.go |
71 |
19 |
87% |
command/check_version.go |
50 |
17 |
84% |
patching/patching.go |
134 |
54 |
79% |
provisioners/core.go |
385 |
180 |
76% |
provisioners/templateprov/template.go |
290 |
163 |
72% |
command/init.go |
321 |
201 |
68% |
A few specifics that affect the API shape if this moves forward.
I already mentioned the ProvisionResources signature difference (*compose.Project vs namespace string). Whatever resolution we pick has to land before Phase 1 because everything downstream depends on it.
The <mode> arg substitution in score-k8s's cmdprov is the kind of divergence that grew quietly because nobody had to coordinate on changes. There may be others I haven't seen. I only looked carefully at the files I read for #496 and at the ones that surfaced in the similarity table; I didn't do an exhaustive feature inventory.
Tests in internal/provisioners of each consumer would need to either move with the code or get reproduced as smoke tests against the lifted package. That's work, but the test count isn't huge and most of it is concrete enough to translate mechanically.
I want to be explicit on one thing: I'm not asking for commit access to score-go or proposing to take over a package. The long-term ownership stays with the four of you. I can drive the lift PR, but once it's merged you maintain it.
Two things I'd want a steer on before starting. Is there a reason the duplication has been left in place that I'm not seeing? And how much appetite is there for expanding score-go's public API to accommodate provisioner machinery? The first question is a sanity check; the second is the load-bearing one.
Verification commands. Anyone can reproduce the similarity table:
git clone https://github.com/score-spec/score-compose.git /tmp/score-compose
git clone https://github.com/score-spec/score-k8s.git /tmp/score-k8s
for f in $(find /tmp/score-compose/internal -name "*.go" -not -name "*_test.go" \
| sed 's|/tmp/score-compose/internal/||'); do
k8s_path="/tmp/score-k8s/internal/$f"
if [ -f "$k8s_path" ]; then
lines=$(wc -l < "/tmp/score-compose/internal/$f")
diff_n=$(diff "/tmp/score-compose/internal/$f" "$k8s_path" 2>/dev/null | grep -c "^[<>]")
pct=$((100 * (lines - diff_n/2) / lines))
printf "%-55s lines=%4d diff=%4d ~%d%% identical\n" "$f" "$lines" "$diff_n" "$pct"
fi
done | sort -t= -k4 -rn
And the path-traversal still being live in score-k8s as I write this:
grep -n "filepath.IsAbs\|filepath.IsLocal\|os.ReadFile" \
/tmp/score-k8s/internal/convert/container_files.go
# line 66 has IsAbs but no IsLocal; line 69 reads the file.
Is your feature request related to a problem? Please describe.
Last week I sent score-compose#496 to fix a path-traversal in
containers[].files[].source. Before opening the PR I checked whether the same pattern existed elsewhere in the org. It does.score-go/loader/normalize.go:54has the same vulnerable readFile helper, andscore-k8s/internal/convert/container_files.go:66has the same code in line-for-line form. So one bug, three independent fixes.That got me curious about how much of the code overlaps between the two active reference implementations, so I ran a per-file diff over
score-compose/internalandscore-k8s/internal. The numbers were higher than I expected. Out of 14 files that have a counterpart in both repos, five are 90% identical or better.logging/logging.gois byte-identical. The orchestration loop atprovisioners/core.gois 385 lines and 76% identical, with most of the divergence being which target-specific types it composes against.So the path traversal living in three places isn't an accident. It's a symptom of the fact that score-compose and score-k8s carry roughly 2,700 lines of nearly-the-same Go in their
internal/trees, evolving independently. score-compose#489 (the work to makeenvprova loadable provisioner type) is the second symptom I tripped over: it's a useful feature that score-k8s users currently can't get because the change hasn't been mirrored.This isn't blame. The maintainers across all three repos are the same four people; the drift exists despite attention, not because of inattention. It's structural.
Describe the solution you'd like
Lift the parts of
internal/provisionersandinternal/commandthat are already nearly identical into score-go, so both consumers depend on one canonical version instead of two evolving copies.The three things I'd start with, in order:
The
Provisionerinterface and theProvisionResourcesorchestrator. Currentlyprovisioners/core.goin both repos. About 385 lines, 76% identical. It's already generic overframework.State[Extras]which lives in score-go, so the actual code move is mostly mechanical. The non-trivial part is that score-compose's version takes a*compose.Projectas its last parameter and score-k8s's takes anamespace string. I think the cleanest thing is to push both target-specific args into the existingState[Extras].Extrasfield, which was designed for this exact case, but that's worth bikeshedding an interface likeProvisionTargetwith two concrete implementations would also work.cmdprovandtemplateprov.cmdprovis the easier one: 197 lines, 96% identical. The only real divergence is a 9-line<mode>arg substitution score-k8s added at lines 152-160 of its copy. That can be a singleOptions.ArgReplacements map[string]stringfield on the provisioner, nil in score-compose, populated with{"<mode>": "provision"}in score-k8s.templateprovis harder: 290 lines, 72% identical, and the remaining 28% is genuinely target-specific (compose vs k8s template outputs). Sotemplateprovis an extract-the-shared-bits exercise rather than a full move.The
.provisioners.yamlloader.provisioners/loader/load.go, 148 lines, 95% identical, with no behavior that's actually implementation-specific. This is the simplest of the three and could land as one PR.After all three, each consumer drops somewhere around 2,000 lines from its
internal/tree. Any future Score implementation (score-aca and score-radius are in progress; third-party forks may follow) inherits the orchestration plus the built-in provisioners for free instead of copying them.What I don't want this to do: pull target-specific render code into score-go.
score-compose/internal/compose/convert.goandscore-k8s/internal/convert/stay where they are. The orchestration framework moves; the docker-compose-vs-k8s rendering doesn't.How I'd want to roll this out: as one PR against score-go for Phase 1, get it merged, bump both downstream repos to use it (with adapter shims in their existing
internal/provisioners/core.gofor one release cycle so there's no hard cutover), and revisit Phase 2 based on how that landed. I don't want to commit to weeks of work in writing. I have the context from #496 and from the score-k8s mirror PR I'm sending shortly, and I'd rather not write the same patch a fourth time when the next file-handling bug shows up.Describe alternatives you've considered
The two alternatives I actually weighed.
Keep the status quo and rely on cross-repo PR habits. This is the current model. Whoever ships a change in score-compose remembers to mirror it in score-k8s, and vice versa. It hasn't held up. The two examples I cited are the ones I personally tripped over in a two-week window; older drift cases probably exist that I didn't go looking for. The missing ingredient isn't maintainer attention. It's a single canonical home for the shared code.
A cross-implementation conformance test suite. A separate repo with canonical Score files plus expected behaviors that each implementation runs in CI. This detects drift after it happens, which has value, but it doesn't prevent drift, and it adds a whole new maintained artifact (the kit, the runners, an expectation DSL). I think a conformance kit is worth doing eventually, just not first if the shared code is unified, the kit's job gets considerably smaller because it only has to test the bits that legitimately differ.
There's also a "leave it alone because score-go is intentionally narrow" position that's a real one. If the maintainers' view is that score-go is intentionally scoped to parsing, schema, and framework primitives, and that provisioner machinery is intentionally an implementation-level concern, then duplication is the price of that separation, and that's defensible. I don't have a strong opinion on which side is right. That's the question I'd want answered before writing any code.
Additional context
The per-file similarity numbers from running
diffover bothinternal/trees (commands at the bottom of this issue if you want to reproduce):internal/)logging/logging.gocommand/version.goprovisioners/cmdprov/commandprov.goprovisioners/loader/load.gocommand/provisioners.gocommand/resources.goversion/version.gocommand/check_version.gopatching/patching.goprovisioners/core.goprovisioners/templateprov/template.gocommand/init.goA few specifics that affect the API shape if this moves forward.
I already mentioned the
ProvisionResourcessignature difference (*compose.Projectvsnamespace string). Whatever resolution we pick has to land before Phase 1 because everything downstream depends on it.The
<mode>arg substitution in score-k8s'scmdprovis the kind of divergence that grew quietly because nobody had to coordinate on changes. There may be others I haven't seen. I only looked carefully at the files I read for #496 and at the ones that surfaced in the similarity table; I didn't do an exhaustive feature inventory.Tests in
internal/provisionersof each consumer would need to either move with the code or get reproduced as smoke tests against the lifted package. That's work, but the test count isn't huge and most of it is concrete enough to translate mechanically.I want to be explicit on one thing: I'm not asking for commit access to score-go or proposing to take over a package. The long-term ownership stays with the four of you. I can drive the lift PR, but once it's merged you maintain it.
Two things I'd want a steer on before starting. Is there a reason the duplication has been left in place that I'm not seeing? And how much appetite is there for expanding score-go's public API to accommodate provisioner machinery? The first question is a sanity check; the second is the load-bearing one.
Verification commands. Anyone can reproduce the similarity table:
And the path-traversal still being live in score-k8s as I write this: