Skip to content

Commit 52f0465

Browse files
committed
refactor: enhance artifact handling and validation logic
1 parent 242b79e commit 52f0465

8 files changed

Lines changed: 416 additions & 72 deletions

File tree

‎models/artefacts.go‎

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import (
55
"strings"
66
)
77

8-
// ArtifactKind is how a component reaches an installation: pulled from a
9-
// registry, or downloaded as a file.
8+
// ArtifactKind is what artefacts exist for a component: a pulled image, a
9+
// downloaded file, or both.
1010
//
1111
// It is a property of the COMPONENT — fixed by how it is built and shipped —
1212
// and not of the request that mentions it. Deriving it from which fields a
@@ -24,6 +24,18 @@ const (
2424
// Files have no repository and no tag, so the whole tag-and-channel half of
2525
// update resolution is inapplicable to them.
2626
ArtifactKindFile ArtifactKind = "file"
27+
28+
// ArtifactKindFileImage is both: pulled from a registry like an image, and
29+
// published as a downloadable executable like a file.
30+
//
31+
// pentagi is one. An installer pulls the container and reports it under
32+
// `images`; a running product reports the executable under `files` and can
33+
// replace it without waiting for a pull. The two facts are one kind, not a
34+
// kind plus a side table: moving pentagi to ArtifactKindFile would take it
35+
// out of every update check's `images` list, and leaving it as
36+
// ArtifactKindImage would refuse it under `files` and refuse the download
37+
// of its own executable.
38+
ArtifactKindFileImage ArtifactKind = "file-image"
2739
)
2840

2941
func (ak ArtifactKind) String() string {
@@ -32,13 +44,33 @@ func (ak ArtifactKind) String() string {
3244

3345
func (ak ArtifactKind) Valid() error {
3446
switch ak {
35-
case ArtifactKindImage, ArtifactKindFile:
47+
case ArtifactKindImage, ArtifactKindFile, ArtifactKindFileImage:
3648
return nil
3749
default:
3850
return fmt.Errorf("invalid ArtifactKind: %s", ak)
3951
}
4052
}
4153

54+
// HasFile reports whether a downloadable file is published for this kind.
55+
func (ak ArtifactKind) HasFile() bool {
56+
switch ak {
57+
case ArtifactKindFile, ArtifactKindFileImage:
58+
return true
59+
default:
60+
return false
61+
}
62+
}
63+
64+
// HasImage reports whether this kind is pulled from a registry.
65+
func (ak ArtifactKind) HasImage() bool {
66+
switch ak {
67+
case ArtifactKindImage, ArtifactKindFileImage:
68+
return true
69+
default:
70+
return false
71+
}
72+
}
73+
4274
// componentArtifactKinds is THE table. Every component of AllComponentTypes has
4375
// an entry here, and TestEveryComponentHasAnArtifactKind fails when one does
4476
// not.
@@ -59,9 +91,12 @@ var componentArtifactKinds = map[ComponentType]ArtifactKind{
5991
ComponentTypeEngineScenario: ArtifactKindFile,
6092
ComponentTypeJaegerClickhouse: ArtifactKindFile,
6193

94+
// Both: the installer pulls a container, and a running product can also
95+
// download the executable of the same build.
96+
ComponentTypePentagi: ArtifactKindFileImage,
97+
6298
// Delivered as images: everything published to a registry, plus every
6399
// third-party service a stack starts from one.
64-
ComponentTypePentagi: ArtifactKindImage,
65100
ComponentTypeScraper: ArtifactKindImage,
66101
ComponentTypeWorker: ArtifactKindImage,
67102
ComponentTypePgvector: ArtifactKindImage,
@@ -97,28 +132,34 @@ func (ct ComponentType) ArtifactKind() ArtifactKind {
97132
return ArtifactKindImage
98133
}
99134

100-
// IsFileComponent reports whether this component is delivered as a file.
135+
// IsFileComponent reports whether this component can appear under `files` and
136+
// whether a package of it can be downloaded. FileImage components are included:
137+
// they have that file in addition to the image an installer pulls.
101138
func (ct ComponentType) IsFileComponent() bool {
102-
return ct.ArtifactKind() == ArtifactKindFile
139+
return ct.ArtifactKind().HasFile()
103140
}
104141

105-
// IsImageComponent reports whether this component is delivered as an image.
142+
// IsImageComponent reports whether this component can appear under `images`.
143+
// FileImage components are included: that is how an installer delivers them.
106144
func (ct ComponentType) IsImageComponent() bool {
107-
return ct.ArtifactKind() == ArtifactKindImage
145+
return ct.ArtifactKind().HasImage()
108146
}
109147

110-
// FileComponentTypes and ImageComponentTypes are the vocabulary split by kind,
111-
// in vocabulary order. Derived from the table rather than written twice, so
112-
// there is no second list to forget.
148+
// FileComponentTypes and ImageComponentTypes are the vocabulary split by which
149+
// artefacts exist, in vocabulary order. Derived from the table rather than
150+
// written twice, so there is no second list to forget.
151+
//
152+
// They overlap: a FileImage component is in both, which is the whole point of
153+
// that kind.
113154
var (
114-
FileComponentTypes = componentsOfKind(ArtifactKindFile)
115-
ImageComponentTypes = componentsOfKind(ArtifactKindImage)
155+
FileComponentTypes = componentsMatching(ArtifactKind.HasFile)
156+
ImageComponentTypes = componentsMatching(ArtifactKind.HasImage)
116157
)
117158

118-
func componentsOfKind(kind ArtifactKind) []ComponentType {
159+
func componentsMatching(match func(ArtifactKind) bool) []ComponentType {
119160
out := make([]ComponentType, 0, len(AllComponentTypes))
120161
for _, ct := range AllComponentTypes {
121-
if ct.ArtifactKind() == kind {
162+
if match(ct.ArtifactKind()) {
122163
out = append(out, ct)
123164
}
124165
}
@@ -128,8 +169,12 @@ func componentsOfKind(kind ArtifactKind) []ComponentType {
128169
// FileComponentEnum is the file vocabulary as a comma-separated list, the form
129170
// documentation and hand-written enumerations have to match.
130171
func FileComponentEnum() string {
131-
names := make([]string, 0, len(FileComponentTypes))
132-
for _, ct := range FileComponentTypes {
172+
return componentEnum(FileComponentTypes)
173+
}
174+
175+
func componentEnum(components []ComponentType) string {
176+
names := make([]string, 0, len(components))
177+
for _, ct := range components {
133178
names = append(names, ct.String())
134179
}
135180
return strings.Join(names, ",")

‎models/artefacts_test.go‎

Lines changed: 87 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,17 @@ func TestEveryComponentHasAnArtifactKind(t *testing.T) {
3333
}
3434
}
3535

36-
// TestTheFileVocabularyIsExactlyTheDeliveredFiles pins the split itself, as a
37-
// literal, on both sides of the contract.
36+
// TestTheFileVocabularyIsExactlyTheDeliveredFiles pins which components have a
37+
// published file — the file-only ones plus FileImage — as a literal on both
38+
// sides of the contract.
3839
func TestTheFileVocabularyIsExactlyTheDeliveredFiles(t *testing.T) {
3940
want := []ComponentType{
4041
ComponentTypeInstaller,
4142
ComponentTypeEngine,
4243
ComponentTypeJaegerClickhouse,
4344
ComponentTypeEngineRegistry,
4445
ComponentTypeEngineScenario,
46+
ComponentTypePentagi,
4547
}
4648
got := map[ComponentType]bool{}
4749
for _, ct := range FileComponentTypes {
@@ -56,22 +58,34 @@ func TestTheFileVocabularyIsExactlyTheDeliveredFiles(t *testing.T) {
5658
for ct := range got {
5759
t.Errorf("%s is delivered as a file but is not in the pinned list", ct)
5860
}
59-
if n := len(FileComponentTypes) + len(ImageComponentTypes); n != len(AllComponentTypes) {
60-
t.Errorf("the two kinds cover %d components, the vocabulary has %d", n, len(AllComponentTypes))
61+
// FileImage sits in both lists, so the two lengths sum to more than the
62+
// vocabulary. Every component must still appear in at least one.
63+
seen := map[ComponentType]struct{}{}
64+
for _, ct := range FileComponentTypes {
65+
seen[ct] = struct{}{}
66+
}
67+
for _, ct := range ImageComponentTypes {
68+
seen[ct] = struct{}{}
69+
}
70+
if len(seen) != len(AllComponentTypes) {
71+
t.Errorf("the two kinds cover %d components, the vocabulary has %d",
72+
len(seen), len(AllComponentTypes))
6173
}
6274
}
6375

6476
// TestOnlyAFileHasAPackage exercises the `filecomp` tag over the WHOLE
6577
// vocabulary — the shape that catches an omission rather than one that confirms
6678
// the members already present.
6779
//
68-
// The list this replaced named two of the five, so an SDK asking for
69-
// jaeger-clickhouse, engine-registry or engine-scenario refused locally before
70-
// the request was ever sent.
80+
// The list this replaced named two of the five file-only components, so an SDK
81+
// asking for jaeger-clickhouse, engine-registry or engine-scenario refused
82+
// locally before the request was ever sent. Asking only the image/file delivery
83+
// question then cost one more: pentagi is FileImage, and treating that as "not
84+
// a file" refused the executable a running product replaces itself with.
7185
//
72-
// BOTH package requests are swept. Gating one and not the other is a real
73-
// state this test exists to forbid: the SDK would send a metadata request the
74-
// service refuses, while refusing the download of that same artefact itself.
86+
// BOTH package requests are swept. Gating one and not the other is a real state
87+
// this test exists to forbid: the SDK would send a metadata request the service
88+
// refuses, while refusing the download of that same artefact itself.
7589
func TestOnlyAFileHasAPackage(t *testing.T) {
7690
for _, ct := range AllComponentTypes {
7791
requests := map[string]IValid{
@@ -90,20 +104,71 @@ func TestOnlyAFileHasAPackage(t *testing.T) {
90104
}
91105
for name, request := range requests {
92106
err := request.Valid()
93-
switch ct.ArtifactKind() {
94-
case ArtifactKindFile:
95-
if err != nil {
96-
t.Errorf("%s is delivered as a file but %s rejects it: %v", ct, name, err)
97-
}
98-
case ArtifactKindImage:
99-
if err == nil {
100-
t.Errorf("%s is an image and has no package, but %s accepted it", ct, name)
101-
}
107+
if ct.IsFileComponent() && err != nil {
108+
t.Errorf("%s has a published file but %s rejects it: %v", ct, name, err)
109+
}
110+
if !ct.IsFileComponent() && err == nil {
111+
t.Errorf("%s has no package, but %s accepted it", ct, name)
102112
}
103113
}
104114
}
105115
}
106116

117+
// TestPentagiIsFileImage pins the one component that is both an image an
118+
// installer pulls and a file a running product downloads.
119+
//
120+
// It must stay in BOTH update-check lists: under `images` the service resolves
121+
// it against its registry reference, under `files` against the published
122+
// executable. Moving it to ArtifactKindFile takes it out of every installer's
123+
// image list; leaving it as ArtifactKindImage refuses the file half. FileImage
124+
// is both answers in the table, not a kind plus a side set.
125+
func TestPentagiIsFileImage(t *testing.T) {
126+
if got := ComponentTypePentagi.ArtifactKind(); got != ArtifactKindFileImage {
127+
t.Errorf("pentagi must be file-image, got %s", got)
128+
}
129+
if !ComponentTypePentagi.IsImageComponent() {
130+
t.Error("pentagi must stay an image: an update check resolves it as one")
131+
}
132+
if !ComponentTypePentagi.IsFileComponent() {
133+
t.Error("pentagi ships an executable a running product can replace itself with")
134+
}
135+
136+
base := CheckUpdatesRequest{
137+
InstallerVersion: "1.0.0",
138+
InstallerOS: OSTypeLinux,
139+
InstallerArch: ArchTypeAMD64,
140+
Strategy: UpdateStrategyPreview,
141+
}
142+
asFile := base
143+
asFile.Files = []FileComponentInfo{{
144+
Component: ComponentTypePentagi,
145+
Status: ComponentStatusRunning,
146+
OS: OSTypeLinux,
147+
Arch: ArchTypeAMD64,
148+
}}
149+
if err := asFile.Valid(); err != nil {
150+
t.Errorf("pentagi reported under `files` must validate: %v", err)
151+
}
152+
asImage := base
153+
asImage.Images = []ImageComponentInfo{{
154+
Component: ComponentTypePentagi,
155+
Status: ComponentStatusRunning,
156+
OS: OSTypeLinux,
157+
Arch: ArchTypeAMD64,
158+
Repository: "vxcontrol/pentagi",
159+
Tag: "latest",
160+
}}
161+
if err := asImage.Valid(); err != nil {
162+
t.Errorf("pentagi reported under `images` must validate: %v", err)
163+
}
164+
both := base
165+
both.Images = asImage.Images
166+
both.Files = asFile.Files
167+
if err := both.Valid(); err != nil {
168+
t.Errorf("pentagi reported under both lists must validate: %v", err)
169+
}
170+
}
171+
107172
// TestAComponentReportedUnderTheWrongKindIsRejected closes the hole that
108173
// splitting the request lists left open.
109174
//
@@ -126,17 +191,17 @@ func TestAComponentReportedUnderTheWrongKindIsRejected(t *testing.T) {
126191
t.Run("an image reported under files", func(t *testing.T) {
127192
request := base()
128193
request.Files = []FileComponentInfo{{
129-
Component: ComponentTypePentagi,
194+
Component: ComponentTypeScraper,
130195
Status: ComponentStatusRunning,
131196
OS: OSTypeLinux,
132197
Arch: ArchTypeAMD64,
133198
Version: &version,
134199
}}
135200
err := request.Valid()
136201
if err == nil {
137-
t.Fatal("pentagi is an image; reporting it as a file must not validate")
202+
t.Fatal("scraper is an image; reporting it as a file must not validate")
138203
}
139-
if !strings.Contains(err.Error(), "pentagi") {
204+
if !strings.Contains(err.Error(), "scraper") {
140205
t.Errorf("the error does not name the component: %v", err)
141206
}
142207
})

0 commit comments

Comments
 (0)