Skip to content

Commit c8cbe55

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

File tree

18 files changed

+1737
-78
lines changed

18 files changed

+1737
-78
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+
// DefaultInstanceName is the name of the default instance into which the app is deployed when no
247+
// specific instance is passed.
248+
const DefaultInstanceName = "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+
// Instances are the deployed instances.
274+
Instances map[string]*Instance `yaml:"instances,omitempty" json:"instances,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, instance := range d.Instances {
298+
if err := instance.Validate(); err != nil {
299+
return fmt.Errorf("bad instance '%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+
// Instance is a hosted instance where a ROLF app is deployed.
311+
type Instance 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+
// Instance is the identifier of the instance to deploy into.
317+
Instance string `yaml:"instance,omitempty" json:"instance,omitempty"`
318+
}
319+
320+
// Validate validates the instance for correctness.
321+
func (i *Instance) Validate() error {
322+
if i.Offer != "" && i.Provider == "" {
323+
return fmt.Errorf("offer identifier cannot be specified without a provider")
324+
}
325+
if i.Instance != "" && i.Provider == "" {
326+
return fmt.Errorf("instance 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: 31 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"
@@ -24,53 +27,63 @@ const (
2427
)
2528

2629
// PushBundleToOciRepository pushes an ORC bundle to the given remote OCI repository.
27-
func PushBundleToOciRepository(bundleFn, dst, tag string) error {
30+
//
31+
// Returns the manifest digest.
32+
func PushBundleToOciRepository(bundleFn, dst string) (string, error) {
2833
ctx := context.Background()
2934

35+
atoms := strings.Split(dst, ":")
36+
if len(atoms) != 2 {
37+
return "", fmt.Errorf("malformed OCI repository reference (repo:tag required)")
38+
}
39+
dst = atoms[0]
40+
tag := atoms[1]
41+
3042
// Open the bundle.
3143
bnd, err := bundle.Open(bundleFn)
3244
if err != nil {
33-
return fmt.Errorf("failed to open bundle: %w", err)
45+
return "", fmt.Errorf("failed to open bundle: %w", err)
3446
}
3547
defer bnd.Close()
3648

3749
// Create a temporary file store to build the OCI layers.
3850
tmpDir, err := os.MkdirTemp("", "oasis-orc2oci")
3951
if err != nil {
40-
return fmt.Errorf("failed to create temporary directory: %w", err)
52+
return "", fmt.Errorf("failed to create temporary directory: %w", err)
4153
}
4254
defer os.RemoveAll(tmpDir)
4355

4456
storeDir := filepath.Join(tmpDir, "oci")
4557
store, err := file.New(storeDir)
4658
if err != nil {
47-
return fmt.Errorf("failed to create temporary OCI store: %w", err)
59+
return "", fmt.Errorf("failed to create temporary OCI store: %w", err)
4860
}
4961
defer store.Close()
5062

5163
bundleDir := filepath.Join(tmpDir, "bundle")
5264
if err = bnd.WriteExploded(bundleDir); err != nil {
53-
return fmt.Errorf("failed to explode bundle: %w", err)
65+
return "", fmt.Errorf("failed to explode bundle: %w", err)
5466
}
5567

5668
// Generate the config object from the manifest.
5769
const manifestName = "META-INF/MANIFEST.MF"
5870
configDsc, err := store.Add(ctx, manifestName, ociTypeOrcConfig, filepath.Join(bundleDir, manifestName))
5971
if err != nil {
60-
return fmt.Errorf("failed to add config object from manifest: %w", err)
72+
return "", fmt.Errorf("failed to add config object from manifest: %w", err)
6173
}
6274

6375
// Add other files as layers.
6476
layers := make([]v1.Descriptor, 0, len(bnd.Data)-1)
65-
for fn := range bnd.Data {
77+
fns := slices.Sorted(maps.Keys(bnd.Data)) // Ensure deterministic order.
78+
for _, fn := range fns {
6679
if fn == manifestName {
6780
continue
6881
}
6982

7083
var layerDsc v1.Descriptor
7184
layerDsc, err = store.Add(ctx, fn, ociTypeOrcLayer, filepath.Join(bundleDir, fn))
7285
if err != nil {
73-
return fmt.Errorf("failed to add OCI layer: %w", err)
86+
return "", fmt.Errorf("failed to add OCI layer: %w", err)
7487
}
7588

7689
layers = append(layers, layerDsc)
@@ -80,25 +93,29 @@ func PushBundleToOciRepository(bundleFn, dst, tag string) error {
8093
opts := oras.PackManifestOptions{
8194
Layers: layers,
8295
ConfigDescriptor: &configDsc,
96+
ManifestAnnotations: map[string]string{
97+
// Use a fixed crated timestamp to avoid changing the manifest digest for no reason.
98+
v1.AnnotationCreated: "2025-03-31T00:00:00Z",
99+
},
83100
}
84101
manifestDescriptor, err := oras.PackManifest(ctx, store, oras.PackManifestVersion1_1, ociTypeOrcArtifact, opts)
85102
if err != nil {
86-
return fmt.Errorf("failed to pack OCI manifest: %w", err)
103+
return "", fmt.Errorf("failed to pack OCI manifest: %w", err)
87104
}
88105

89106
// Tag the manifest.
90107
if err = store.Tag(ctx, manifestDescriptor, tag); err != nil {
91-
return fmt.Errorf("failed to tag OCI manifest: %w", err)
108+
return "", fmt.Errorf("failed to tag OCI manifest: %w", err)
92109
}
93110

94111
// Connect to remote repository.
95112
repo, err := remote.NewRepository(dst)
96113
if err != nil {
97-
return fmt.Errorf("failed to init remote OCI repository: %w", err)
114+
return "", fmt.Errorf("failed to init remote OCI repository: %w", err)
98115
}
99116
creds, err := credentials.NewStoreFromDocker(credentials.StoreOptions{})
100117
if err != nil {
101-
return fmt.Errorf("failed to init OCI credential store: %w", err)
118+
return "", fmt.Errorf("failed to init OCI credential store: %w", err)
102119
}
103120
repo.Client = &auth.Client{
104121
Client: retry.DefaultClient,
@@ -108,8 +125,8 @@ func PushBundleToOciRepository(bundleFn, dst, tag string) error {
108125

109126
// Push to remote repository.
110127
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)
128+
return "", fmt.Errorf("failed to push to remote OCI repository: %w", err)
112129
}
113130

114-
return nil
131+
return manifestDescriptor.Digest.String(), nil
115132
}

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)