Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions boot/components/core/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func All() []boot.Component {
Dispatcher(),
WASMIsolation(),
Profiler(),
Artifacts(),
Registry(),
Finder(),
Security(),
Expand Down
26 changes: 26 additions & 0 deletions boot/components/core/artifact.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: MPL-2.0

package core

import (
"context"

"github.com/wippyai/runtime/api/boot"
"github.com/wippyai/runtime/boot/deps/artifact"
"github.com/wippyai/runtime/boot/deps/artifact/standard"
)

// Artifacts composes the artifact formats available to dependency lifecycle
// operations. Formats are explicit boot dependencies rather than globals.
func Artifacts() boot.Component {
return boot.New(boot.P{
Name: ArtifactName,
Load: func(ctx context.Context) (context.Context, error) {
registry, err := standard.NewRegistry()
if err != nil {
return ctx, err
}
return artifact.WithRegistry(ctx, registry), nil
},
})
}
26 changes: 26 additions & 0 deletions boot/components/core/artifact_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: MPL-2.0

package core

import (
"testing"

"github.com/stretchr/testify/require"
bootpkg "github.com/wippyai/runtime/boot"
"github.com/wippyai/runtime/boot/deps/artifact"
"go.uber.org/zap"
)

func TestArtifactsBootRegistration(t *testing.T) {
ctx, err := bootpkg.NewBootstrapContext(zap.NewNop(), nil)
require.NoError(t, err)
loader, err := bootpkg.NewLoader(Artifacts())
require.NoError(t, err)
ctx, err = loader.Load(ctx)
require.NoError(t, err)

registry := artifact.GetRegistry(ctx)
require.NotNil(t, registry)
_, registered := registry.Resolve("node-package")
require.True(t, registered)
}
3 changes: 3 additions & 0 deletions boot/components/core/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const (
PIDGenName boot.Name = "pidgen"
SecurityName boot.Name = "security"
SecurityPolicyName boot.Name = "security.policy"
ArtifactName boot.Name = "artifact"
RegistryName boot.Name = "registry"
FinderName boot.Name = "finder"
SupervisorName boot.Name = "supervisor"
Expand Down Expand Up @@ -49,4 +50,6 @@ const (
RegistryDependencyLockPath boot.Name = "dependency_lock_path"
// RegistryDependencyVendorDir overrides vendor directory for dependency installs.
RegistryDependencyVendorDir boot.Name = "dependency_vendor_dir"
// RegistryDependencyArtifactRoot overrides the root for materialized dependency artifacts.
RegistryDependencyArtifactRoot boot.Name = "dependency_artifact_root"
)
2 changes: 1 addition & 1 deletion boot/components/core/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func TestCorePlugins(t *testing.T) {
t.Error("PID generator not available in context")
}

lifecycleLoader, err := bootpkg.NewLoader(Registry(), Supervisor())
lifecycleLoader, err := bootpkg.NewLoader(Artifacts(), Registry(), Supervisor())
require.NoError(t, err)
ctx, err = lifecycleLoader.Load(ctx)
require.NoError(t, err)
Expand Down
12 changes: 10 additions & 2 deletions boot/components/core/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
logapi "github.com/wippyai/runtime/api/logs"
regapi "github.com/wippyai/runtime/api/registry"
bootpkg "github.com/wippyai/runtime/boot"
"github.com/wippyai/runtime/boot/deps/artifact"
hubdeps "github.com/wippyai/runtime/boot/deps/hub"
"github.com/wippyai/runtime/boot/deps/lock"
"github.com/wippyai/runtime/system/registry"
Expand All @@ -33,7 +34,7 @@ func Registry() boot.Component {

return boot.New(boot.P{
Name: RegistryName,
DependsOn: []boot.Name{},
DependsOn: []boot.Name{ArtifactName},
Load: func(ctx context.Context) (context.Context, error) {
logger := logapi.GetLogger(ctx).Named("registry")
bus := event.GetBus(ctx)
Expand Down Expand Up @@ -119,7 +120,7 @@ func Registry() boot.Component {

registryOpts := []registry.Option{}

depHandler, err := newDependencyHandler(cfg, logger.Named("dependency"), resolver)
depHandler, err := newDependencyHandler(ctx, cfg, logger.Named("dependency"), resolver)
if err != nil {
logger.Warn("dependency handler disabled", zap.Error(err))
} else if depHandler != nil {
Expand Down Expand Up @@ -220,6 +221,7 @@ func readKindSlice(cfg boot.Config, key boot.Name) ([]regapi.Kind, bool) {
}

func newDependencyHandler(
ctx context.Context,
cfg boot.Config,
logger *zap.Logger,
resolver regapi.DependencyResolver,
Expand All @@ -233,6 +235,11 @@ func newDependencyHandler(
Logger: logger,
Resolver: resolver,
}
artifactRegistry := artifact.GetRegistry(ctx)
if artifactRegistry == nil {
return nil, fmt.Errorf("artifact registry is not initialized")
}
opts.Artifacts = artifactRegistry
workspaceReplacements, err := lock.WorkspaceReplacements(cfg)
if err != nil {
return nil, fmt.Errorf("load workspace replacements: %w", err)
Expand All @@ -244,6 +251,7 @@ func newDependencyHandler(
opts.DownloadTimeout = registryCfg.GetDuration(RegistryDependencyDownloadTimeout, 0)
opts.LockPath = registryCfg.GetString(RegistryDependencyLockPath, "")
opts.VendorDir = registryCfg.GetString(RegistryDependencyVendorDir, "")
opts.ArtifactRoot = registryCfg.GetString(RegistryDependencyArtifactRoot, "")
}

return hubdeps.NewDependencyHandler(opts)
Expand Down
2 changes: 1 addition & 1 deletion boot/components/core/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func TestRegistryPostgresHistoryRequiresDSN(t *testing.T) {
ctx, err := bootpkg.NewBootstrapContext(zap.NewNop(), cfg)
require.NoError(t, err)

loader, err := bootpkg.NewLoader(Registry())
loader, err := bootpkg.NewLoader(Artifacts(), Registry())
require.NoError(t, err)

_, err = loader.Load(ctx)
Expand Down
33 changes: 33 additions & 0 deletions boot/deps/artifact/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MPL-2.0

package artifact

import (
"context"

ctxapi "github.com/wippyai/runtime/api/context"
)

var registryKey = &ctxapi.Key{Name: "artifact.registry"}

// WithRegistry stores the boot-composed artifact registry.
func WithRegistry(ctx context.Context, registry *Registry) context.Context {
appCtx := ctxapi.AppFromContext(ctx)
if appCtx == nil {
return ctx
}
if appCtx.Get(registryKey) == nil {
appCtx.With(registryKey, registry)
}
return ctx
}

// GetRegistry retrieves the boot-composed artifact registry.
func GetRegistry(ctx context.Context) *Registry {
appCtx := ctxapi.AppFromContext(ctx)
if appCtx == nil {
return nil
}
registry, _ := appCtx.Get(registryKey).(*Registry)
return registry
}
79 changes: 79 additions & 0 deletions boot/deps/artifact/directory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: MPL-2.0

package artifact

import (
"context"
"errors"
"fmt"
"os"
"path/filepath"

"github.com/wippyai/runtime/api/payload"
regapi "github.com/wippyai/runtime/api/registry"
dirapi "github.com/wippyai/runtime/api/service/fs/directory"
"github.com/wippyai/wapp"
)

const moduleMetadataKey = "module"

// DirectoryResources resolves artifact declarations from selected local
// fs.directory entries. moduleRoots is the authoritative source root for each
// local module; entries outside that set are ignored.
func DirectoryResources(
ctx context.Context,
entries regapi.State,
moduleRoots map[string]string,
moduleVersions map[string]string,
) ([]Resource, error) {
if len(moduleRoots) == 0 {
return nil, nil
}
transcoder := payload.GetTranscoder(ctx)
if transcoder == nil {
return nil, errors.New("payload transcoder is unavailable")
}

resources := make([]Resource, 0)
for _, entry := range entries {
module := entry.Meta.GetString(moduleMetadataKey, "")
moduleRoot, selected := moduleRoots[module]
if !selected || entry.Kind != dirapi.Kind {
continue
}
_, declared, err := ParseDeclaration(wapp.Metadata(entry.Meta))
if err != nil {
return nil, fmt.Errorf("local resource %s: %w", entry.ID.String(), err)
}
if !declared {
continue
}

var cfg dirapi.Config
if err := transcoder.Unmarshal(entry.Data, &cfg); err != nil {
return nil, fmt.Errorf("decode local resource %s: %w", entry.ID.String(), err)
}
if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("validate local resource %s: %w", entry.ID.String(), err)
}
directory := cfg.Directory
if !dirapi.IsConfiguredPathAbsolute(directory) && cfg.Base != dirapi.BaseProject {
directory = filepath.Join(moduleRoot, directory)
}
info, err := os.Stat(directory)
if err != nil {
return nil, fmt.Errorf("inspect local resource %s: %w", entry.ID.String(), err)
}
if !info.IsDir() {
return nil, fmt.Errorf("local resource %s is not a directory", entry.ID.String())
}
resources = append(resources, Resource{
Filesystem: os.DirFS(directory),
Meta: wapp.Metadata(entry.Meta),
ModuleVersion: moduleVersions[module],
ResourceID: wapp.NewID(entry.ID.NS, entry.ID.Name),
Source: directory,
})
}
return resources, nil
}
56 changes: 56 additions & 0 deletions boot/deps/artifact/effect_state_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: MPL-2.0

package artifact

import (
"context"
"os"
"path/filepath"
"testing"
)

func TestRollbackPendingRetriesRestoration(t *testing.T) {
root := t.TempDir()
destination := filepath.Join(root, "npm")
backup := filepath.Join(root, ".npm.artifact-backup-test")
staging := filepath.Join(root, ".npm.artifact-stage-test")
for path, content := range map[string]string{
destination: "new",
backup: "old",
staging: "staged",
} {
if err := os.MkdirAll(path, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(path, "value"), []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}

effect := &Effect{
root: root,
activated: []activatedRoot{{
destination: destination,
backup: backup,
hadTarget: true,
}},
pending: []stagedRoot{{staging: staging}},
state: wappEffectRollbackPending,
}
if err := effect.Rollback(context.Background()); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(filepath.Join(destination, "value"))
if err != nil {
t.Fatal(err)
}
if string(data) != "old" {
t.Fatalf("restored content = %q", data)
}
if _, err := os.Stat(staging); !os.IsNotExist(err) {
t.Fatalf("pending staging directory remains: %v", err)
}
if effect.state != wappEffectRolledBack {
t.Fatalf("state = %d, want rolled back", effect.state)
}
}
49 changes: 49 additions & 0 deletions boot/deps/artifact/filelock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: MPL-2.0

package artifact

import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"time"
)

const artifactLockRetry = 25 * time.Millisecond

func acquireArtifactLock(ctx context.Context, root string) (func() error, error) {
file, err := os.OpenFile(
filepath.Join(root, ".artifacts.lock"),
os.O_CREATE|os.O_RDWR,
0o600,
)
if err != nil {
return nil, fmt.Errorf("open artifact lock: %w", err)
}

for {
unlock, lockErr := tryLockFile(file)
if lockErr == nil {
return func() error {
return errors.Join(unlock(), file.Close())
}, nil
}
if !errors.Is(lockErr, errLockBusy) {
_ = file.Close()
return nil, fmt.Errorf("lock artifact root: %w", lockErr)
}

timer := time.NewTimer(artifactLockRetry)
select {
case <-ctx.Done():
if !timer.Stop() {
<-timer.C
}
_ = file.Close()
return nil, fmt.Errorf("lock artifact root: %w", ctx.Err())
case <-timer.C:
}
}
}
Loading