Skip to content

Commit c1cb32a

Browse files
committed
feat(cmd/rofl): Implement deploy via ROFL market
1 parent 3e8cd7d commit c1cb32a

36 files changed

+1867
-108
lines changed

.golangci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ linters-settings:
7777
- github.com/github/go-spdx/v2
7878
- github.com/opencontainers/image-spec/specs-go/v1
7979
- oras.land/oras-go/v2
80+
- github.com/wI2L/jsondiff
8081
exhaustive:
8182
# Switch statements are to be considered exhaustive if a 'default' case is
8283
# present, even if all enum members aren't listed in the switch.

build/rofl/manifest.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,10 @@ func (m *Manifest) Save() error {
243243
// used in case no deployment is passed.
244244
const DefaultDeploymentName = "default"
245245

246+
// DefaultMachineName is the name of the default machine into which the app is deployed when no
247+
// specific machine is passed.
248+
const DefaultMachineName = "default"
249+
246250
// Deployment describes a single ROFL app deployment.
247251
type Deployment struct {
248252
// AppID is the Bech32-encoded ROFL app ID.
@@ -255,6 +259,8 @@ type Deployment struct {
255259
Admin string `yaml:"admin,omitempty" json:"admin,omitempty"`
256260
// Debug is a flag denoting whether this is a debuggable deployment.
257261
Debug bool `yaml:"debug,omitempty" json:"debug,omitempty"`
262+
// OCIRepository is the optional OCI repository where one can push the ORC to.
263+
OCIRepository string `yaml:"oci_repository,omitempty" json:"oci_repository,omitempty"`
258264
// TrustRoot is the optional trust root configuration.
259265
TrustRoot *TrustRootConfig `yaml:"trust_root,omitempty" json:"trust_root,omitempty"`
260266
// Policy is the ROFL app policy.
@@ -263,9 +269,12 @@ type Deployment struct {
263269
Metadata map[string]string `yaml:"metadata,omitempty" json:"metadata,omitempty"`
264270
// Secrets contains encrypted secrets.
265271
Secrets []*SecretConfig `yaml:"secrets,omitempty" json:"secrets,omitempty"`
272+
273+
// Machines are the machines on which app replicas are deployed.
274+
Machines map[string]*Machine `yaml:"machines,omitempty" json:"machines,omitempty"`
266275
}
267276

268-
// Validate validates the manifest for correctness.
277+
// Validate validates the deployment for correctness.
269278
func (d *Deployment) Validate() error {
270279
if len(d.AppID) > 0 {
271280
var appID rofl.AppID
@@ -284,6 +293,12 @@ func (d *Deployment) Validate() error {
284293
return fmt.Errorf("bad secret: %w", err)
285294
}
286295
}
296+
297+
for name, machine := range d.Machines {
298+
if err := machine.Validate(); err != nil {
299+
return fmt.Errorf("bad machine '%s': %w", name, err)
300+
}
301+
}
287302
return nil
288303
}
289304

@@ -292,6 +307,27 @@ func (d *Deployment) HasAppID() bool {
292307
return len(d.AppID) > 0
293308
}
294309

310+
// Machine is a hosted machine where a ROFL app is deployed.
311+
type Machine struct {
312+
// Provider is the address of the ROFL market provider to deploy to.
313+
Provider string `yaml:"provider,omitempty" json:"provider,omitempty"`
314+
// Offer is the provider's offer identifier to provision.
315+
Offer string `yaml:"offer,omitempty" json:"offer,omitempty"`
316+
// ID is the identifier of the machine to deploy into.
317+
ID string `yaml:"id,omitempty" json:"id,omitempty"`
318+
}
319+
320+
// Validate validates the machine for correctness.
321+
func (m *Machine) Validate() error {
322+
if m.Offer != "" && m.Provider == "" {
323+
return fmt.Errorf("offer identifier cannot be specified without a provider")
324+
}
325+
if m.ID != "" && m.Provider == "" {
326+
return fmt.Errorf("machine identifier cannot be specified without a provider")
327+
}
328+
return nil
329+
}
330+
295331
// TrustRootConfig is the trust root configuration.
296332
type TrustRootConfig struct {
297333
// Height is the consensus layer block height where to take the trust root.

build/rofl/oci.go

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ package rofl
33
import (
44
"context"
55
"fmt"
6+
"maps"
67
"os"
78
"path/filepath"
9+
"slices"
10+
"strings"
811

912
v1 "github.com/opencontainers/image-spec/specs-go/v1"
1013
oras "oras.land/oras-go/v2"
@@ -14,6 +17,7 @@ import (
1417
"oras.land/oras-go/v2/registry/remote/credentials"
1518
"oras.land/oras-go/v2/registry/remote/retry"
1619

20+
"github.com/oasisprotocol/oasis-core/go/common/crypto/hash"
1721
"github.com/oasisprotocol/oasis-core/go/runtime/bundle"
1822
)
1923

@@ -24,53 +28,63 @@ const (
2428
)
2529

2630
// PushBundleToOciRepository pushes an ORC bundle to the given remote OCI repository.
27-
func PushBundleToOciRepository(bundleFn, dst, tag string) error {
31+
//
32+
// Returns the OCI manifest digest and the ORC manifest hash.
33+
func PushBundleToOciRepository(bundleFn, dst string) (string, hash.Hash, error) {
2834
ctx := context.Background()
2935

36+
atoms := strings.Split(dst, ":")
37+
if len(atoms) != 2 {
38+
return "", hash.Hash{}, fmt.Errorf("malformed OCI repository reference (repo:tag required)")
39+
}
40+
dst = atoms[0]
41+
tag := atoms[1]
42+
3043
// Open the bundle.
3144
bnd, err := bundle.Open(bundleFn)
3245
if err != nil {
33-
return fmt.Errorf("failed to open bundle: %w", err)
46+
return "", hash.Hash{}, fmt.Errorf("failed to open bundle: %w", err)
3447
}
3548
defer bnd.Close()
3649

3750
// Create a temporary file store to build the OCI layers.
3851
tmpDir, err := os.MkdirTemp("", "oasis-orc2oci")
3952
if err != nil {
40-
return fmt.Errorf("failed to create temporary directory: %w", err)
53+
return "", hash.Hash{}, fmt.Errorf("failed to create temporary directory: %w", err)
4154
}
4255
defer os.RemoveAll(tmpDir)
4356

4457
storeDir := filepath.Join(tmpDir, "oci")
4558
store, err := file.New(storeDir)
4659
if err != nil {
47-
return fmt.Errorf("failed to create temporary OCI store: %w", err)
60+
return "", hash.Hash{}, fmt.Errorf("failed to create temporary OCI store: %w", err)
4861
}
4962
defer store.Close()
5063

5164
bundleDir := filepath.Join(tmpDir, "bundle")
5265
if err = bnd.WriteExploded(bundleDir); err != nil {
53-
return fmt.Errorf("failed to explode bundle: %w", err)
66+
return "", hash.Hash{}, fmt.Errorf("failed to explode bundle: %w", err)
5467
}
5568

5669
// Generate the config object from the manifest.
5770
const manifestName = "META-INF/MANIFEST.MF"
5871
configDsc, err := store.Add(ctx, manifestName, ociTypeOrcConfig, filepath.Join(bundleDir, manifestName))
5972
if err != nil {
60-
return fmt.Errorf("failed to add config object from manifest: %w", err)
73+
return "", hash.Hash{}, fmt.Errorf("failed to add config object from manifest: %w", err)
6174
}
6275

6376
// Add other files as layers.
6477
layers := make([]v1.Descriptor, 0, len(bnd.Data)-1)
65-
for fn := range bnd.Data {
78+
fns := slices.Sorted(maps.Keys(bnd.Data)) // Ensure deterministic order.
79+
for _, fn := range fns {
6680
if fn == manifestName {
6781
continue
6882
}
6983

7084
var layerDsc v1.Descriptor
7185
layerDsc, err = store.Add(ctx, fn, ociTypeOrcLayer, filepath.Join(bundleDir, fn))
7286
if err != nil {
73-
return fmt.Errorf("failed to add OCI layer: %w", err)
87+
return "", hash.Hash{}, fmt.Errorf("failed to add OCI layer: %w", err)
7488
}
7589

7690
layers = append(layers, layerDsc)
@@ -80,25 +94,29 @@ func PushBundleToOciRepository(bundleFn, dst, tag string) error {
8094
opts := oras.PackManifestOptions{
8195
Layers: layers,
8296
ConfigDescriptor: &configDsc,
97+
ManifestAnnotations: map[string]string{
98+
// Use a fixed crated timestamp to avoid changing the manifest digest for no reason.
99+
v1.AnnotationCreated: "2025-03-31T00:00:00Z",
100+
},
83101
}
84102
manifestDescriptor, err := oras.PackManifest(ctx, store, oras.PackManifestVersion1_1, ociTypeOrcArtifact, opts)
85103
if err != nil {
86-
return fmt.Errorf("failed to pack OCI manifest: %w", err)
104+
return "", hash.Hash{}, fmt.Errorf("failed to pack OCI manifest: %w", err)
87105
}
88106

89107
// Tag the manifest.
90108
if err = store.Tag(ctx, manifestDescriptor, tag); err != nil {
91-
return fmt.Errorf("failed to tag OCI manifest: %w", err)
109+
return "", hash.Hash{}, fmt.Errorf("failed to tag OCI manifest: %w", err)
92110
}
93111

94112
// Connect to remote repository.
95113
repo, err := remote.NewRepository(dst)
96114
if err != nil {
97-
return fmt.Errorf("failed to init remote OCI repository: %w", err)
115+
return "", hash.Hash{}, fmt.Errorf("failed to init remote OCI repository: %w", err)
98116
}
99117
creds, err := credentials.NewStoreFromDocker(credentials.StoreOptions{})
100118
if err != nil {
101-
return fmt.Errorf("failed to init OCI credential store: %w", err)
119+
return "", hash.Hash{}, fmt.Errorf("failed to init OCI credential store: %w", err)
102120
}
103121
repo.Client = &auth.Client{
104122
Client: retry.DefaultClient,
@@ -108,8 +126,8 @@ func PushBundleToOciRepository(bundleFn, dst, tag string) error {
108126

109127
// Push to remote repository.
110128
if _, err = oras.Copy(ctx, store, tag, repo, tag, oras.DefaultCopyOptions); err != nil {
111-
return fmt.Errorf("failed to push to remote OCI repository: %w", err)
129+
return "", hash.Hash{}, fmt.Errorf("failed to push to remote OCI repository: %w", err)
112130
}
113131

114-
return nil
132+
return manifestDescriptor.Digest.String(), bnd.Manifest.Hash(), nil
115133
}

build/rofl/provider/defaults.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package provider
2+
3+
// DefaultSchedulerApp contains the default scheduler app IDs for each network/paratime.
4+
var DefaultSchedulerApp = map[string]map[string]string{
5+
"testnet": {
6+
"sapphire": "rofl1qrqw99h0f7az3hwt2cl7yeew3wtz0fxunu7luyfg",
7+
},
8+
}

0 commit comments

Comments
 (0)