Skip to content
Open
Show file tree
Hide file tree
Changes from all 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)
}
6 changes: 5 additions & 1 deletion boot/components/core/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@

package core

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

const (
// PIDGenName is the name for the PID generator component
PIDGenName boot.Name = "pidgen"
SecurityName boot.Name = "security"
SecurityPolicyName boot.Name = "security.policy"
ArtifactName boot.Name = artifact.ConfigName
RegistryName boot.Name = "registry"
FinderName boot.Name = "finder"
SupervisorName boot.Name = "supervisor"
Expand Down
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 @@ -245,6 +252,7 @@ func newDependencyHandler(
opts.LockPath = registryCfg.GetString(RegistryDependencyLockPath, "")
opts.VendorDir = registryCfg.GetString(RegistryDependencyVendorDir, "")
}
opts.ArtifactRoot = artifact.ConfiguredRoot(cfg, "")

return hubdeps.NewDependencyHandler(opts)
}
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
20 changes: 20 additions & 0 deletions boot/deps/artifact/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: MPL-2.0

package artifact

import "github.com/wippyai/runtime/api/boot"

const (
// ConfigName is the application configuration section owned by artifacts.
ConfigName boot.Name = "artifact"
// ConfigMaterializationRoot overrides the application root for materialized artifacts.
ConfigMaterializationRoot boot.Name = "materialization_root"
)

// ConfiguredRoot returns the configured materialization root or fallback.
func ConfiguredRoot(cfg boot.Config, fallback string) string {
if cfg == nil {
return fallback
}
return cfg.Sub(ConfigName).GetString(ConfigMaterializationRoot, fallback)
}
22 changes: 22 additions & 0 deletions boot/deps/artifact/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: MPL-2.0

package artifact

import (
"testing"

"github.com/wippyai/runtime/api/boot"
)

func TestConfiguredRoot(t *testing.T) {
if got := ConfiguredRoot(nil, ".wippy"); got != ".wippy" {
t.Fatalf("nil config root = %q", got)
}

cfg := boot.NewConfig(boot.WithSection(ConfigName, map[string]any{
ConfigMaterializationRoot: "build/resources",
}))
if got := ConfiguredRoot(cfg, ".wippy"); got != "build/resources" {
t.Fatalf("configured root = %q", got)
}
}
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)
}
}
Loading
Loading