Skip to content

Commit 34db0e8

Browse files
authored
cli: properly resolve composer version from local tree (#311)
Fixes #304 When running local extensions, if the extension manifest does not contain the version (local composer extension), try to find the parent in the tree before falling back to the embedded manifest versions. This will allow older versions of the CLI to work properly with commands like `boe run --local extensions/composer/waf` and properly resolve the versions from the local tree. cc @sjoukedv --------- Signed-off-by: Ignasi Barrera <ignasi@tetrate.io>
1 parent 5d1e7aa commit 34db0e8

3 files changed

Lines changed: 132 additions & 5 deletions

File tree

cli/cmd/run.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,8 @@ func downloadExtensions(ctx context.Context, downloader *extensions.Downloader,
229229
name, extensionSrc, err)
230230
}
231231
manifest.Remote = true // Mark the manifest as remote since it is from a downloaded artifact
232-
// The LoadLocalmanifest resolves "parent" the versions from the embedded manifests, but here, after downloading the source
233-
// we want to force the version to the ones form the downloaded artifact.
232+
// Composer source artifacts contains manifests without version information (just the parent reference).
233+
// We need to set the versions here.
234234
manifest.Version = artifact.Manifest.Version
235235
manifest.ComposerVersion = artifact.Manifest.ComposerVersion
236236

@@ -330,6 +330,10 @@ func loadLocalManifests(ctx context.Context, logger *slog.Logger, downloader *ex
330330
return nil, fmt.Errorf("%w from %s: %w", errFailedToLoadLocalManifest, path, err)
331331
}
332332

333+
if err := extensions.ResolveLocalVersions(manifest); err != nil {
334+
return nil, fmt.Errorf("%w from %s: %w", errFailedToLoadLocalManifest, path, err)
335+
}
336+
333337
if build {
334338
switch manifest.Type {
335339
case extensions.TypeGo:

cli/internal/extensions/manifest.go

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,12 +287,57 @@ func LoadLocalManifest(path string) (*Manifest, error) {
287287
return nil, err
288288
}
289289
m.Path = path
290-
if err = resolveVersions(m, Manifests); err != nil {
291-
return nil, err
292-
}
293290
return m, nil
294291
}
295292

293+
// ResolveLocalVersions resolves version fields for a local manifest that has a parent.
294+
// It first tries to find the parent manifest on the local filesystem by walking up the
295+
// directory tree from the manifest's path. If not found locally, it falls back to the
296+
// embedded manifests.
297+
func ResolveLocalVersions(m *Manifest) error {
298+
if m.Type != TypeGo || m.Parent == "" {
299+
return nil
300+
}
301+
302+
// Try to find the parent manifest on the local filesystem.
303+
parent, err := findLocalParentManifest(m)
304+
if err == nil {
305+
return resolveVersions(m, map[string]*Manifest{parent.Name: parent})
306+
}
307+
308+
// Fall back to the embedded manifests.
309+
return resolveVersions(m, Manifests)
310+
}
311+
312+
// findLocalParentManifest walks up the directory tree from the manifest's path
313+
// looking for a manifest.yaml whose name matches the parent field.
314+
func findLocalParentManifest(m *Manifest) (*Manifest, error) {
315+
// Start from the directory containing the child manifest and walk up.
316+
dir := filepath.Dir(m.Path)
317+
for {
318+
parent := filepath.Dir(dir)
319+
if parent == dir {
320+
// Reached filesystem root without finding the parent.
321+
return nil, fmt.Errorf("%w: %s", ErrParentManifestNotFound, m.Parent)
322+
}
323+
dir = parent
324+
325+
candidate := filepath.Join(dir, "manifest.yaml")
326+
if _, err := os.Stat(candidate); err != nil {
327+
continue
328+
}
329+
330+
// Load without validation since the parent may have a different type (e.g. composer).
331+
cm, err := loadManifest(os.DirFS(dir), "manifest.yaml", false)
332+
if err != nil {
333+
continue
334+
}
335+
if cm.Name == m.Parent {
336+
return cm, nil
337+
}
338+
}
339+
}
340+
296341
// ValidateManifest validates the manifest against the JSON schema.
297342
func ValidateManifest(manifest *Manifest) error {
298343
// Convert manifest to JSON for schema validation

cli/internal/extensions/manifest_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package extensions
77

88
import (
99
"io/fs"
10+
"os"
1011
"path/filepath"
1112
"testing"
1213
"testing/fstest"
@@ -505,3 +506,80 @@ func TestLoadLocalManifest(t *testing.T) {
505506
require.ErrorIs(t, err, ErrParseManifestFile)
506507
})
507508
}
509+
510+
func TestResolveLocalVersions(t *testing.T) {
511+
t.Run("local-parent-found", func(t *testing.T) {
512+
// Create a directory structure: parent/child/manifest.yaml
513+
tmpDir := t.TempDir()
514+
parentDir := tmpDir
515+
childDir := filepath.Join(tmpDir, "child")
516+
require.NoError(t, os.MkdirAll(childDir, 0o750))
517+
518+
parentManifest := `name: test-parent
519+
version: 9.9.9
520+
composerVersion: 9.9.9
521+
minEnvoyVersion: 1.99.0
522+
type: composer
523+
extensionSet: true
524+
`
525+
childManifest := `name: test-child
526+
parent: test-parent
527+
categories: [Misc]
528+
author: Test
529+
description: A child extension
530+
longDescription: A child extension
531+
type: go
532+
tags: [test]
533+
license: Apache-2.0
534+
examples: []
535+
`
536+
require.NoError(t, os.WriteFile(filepath.Join(parentDir, "manifest.yaml"), []byte(parentManifest), 0o600))
537+
require.NoError(t, os.WriteFile(filepath.Join(childDir, "manifest.yaml"), []byte(childManifest), 0o600))
538+
539+
m, err := LoadLocalManifest(filepath.Join(childDir, "manifest.yaml"))
540+
require.NoError(t, err)
541+
require.Empty(t, m.Version)
542+
543+
require.NoError(t, ResolveLocalVersions(m))
544+
assert.Equal(t, "9.9.9", m.Version)
545+
assert.Equal(t, "9.9.9", m.ComposerVersion)
546+
assert.Equal(t, "1.99.0", m.MinEnvoyVersion)
547+
})
548+
549+
t.Run("no-local-parent-falls-back-to-embedded", func(t *testing.T) {
550+
// Create a child manifest in an isolated temp dir (no parent on filesystem).
551+
tmpDir := t.TempDir()
552+
childManifest := `name: test-child
553+
parent: composer
554+
categories: [Misc]
555+
author: Test
556+
description: A child extension
557+
longDescription: A child extension
558+
type: go
559+
tags: [test]
560+
license: Apache-2.0
561+
examples: []
562+
`
563+
require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "manifest.yaml"), []byte(childManifest), 0o600))
564+
565+
m, err := LoadLocalManifest(filepath.Join(tmpDir, "manifest.yaml"))
566+
require.NoError(t, err)
567+
568+
// Should fall back to embedded manifests (composer exists in embedded).
569+
require.NoError(t, ResolveLocalVersions(m))
570+
assert.Equal(t, Manifests["composer"].Version, m.Version)
571+
assert.Equal(t, Manifests["composer"].Version, m.ComposerVersion)
572+
})
573+
574+
t.Run("noop-for-non-go-type", func(t *testing.T) {
575+
m := &Manifest{Name: "test", Type: TypeWasm, Version: "1.0.0"}
576+
require.NoError(t, ResolveLocalVersions(m))
577+
assert.Equal(t, "1.0.0", m.Version)
578+
})
579+
580+
t.Run("noop-for-no-parent", func(t *testing.T) {
581+
m := &Manifest{Name: "test", Type: TypeGo, Version: "1.0.0"}
582+
require.NoError(t, ResolveLocalVersions(m))
583+
assert.Equal(t, "1.0.0", m.Version)
584+
})
585+
}

0 commit comments

Comments
 (0)