Skip to content

Commit 0840d2d

Browse files
committed
feat(cmd/rofl): Add support for custom proxy domains
1 parent f61cef1 commit 0840d2d

File tree

8 files changed

+192
-76
lines changed

8 files changed

+192
-76
lines changed

build/rofl/artifacts.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ var LatestContainerArtifacts = ArtifactsConfig{
1313
Kernel: "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.6.2/stage1.bin#e5d4d654ca1fa2c388bf64b23fc6e67815893fc7cb8b7cfee253d87963f54973",
1414
Stage2: "https://github.com/oasisprotocol/oasis-boot/releases/download/v0.6.2/stage2-podman.tar.bz2#b2ea2a0ca769b6b2d64e3f0c577ee9c08f0bb81a6e33ed5b15b2a7e50ef9a09f",
1515
Container: ContainerArtifactsConfig{
16-
Runtime: "https://github.com/oasisprotocol/oasis-sdk/releases/download/rofl-containers%2Fv0.7.3/rofl-containers#964fbd8edaea8041fd9c5304bb4631b7126d57d06062cc3922e50313cdeef618",
16+
Runtime: "https://github.com/oasisprotocol/oasis-sdk/releases/download/rofl-containers%2Fv0.8.0/rofl-containers#08eb5bbe5df26af276d9a72e9fd7353b3a90b7d27e1cf33e276a82dfd551eec6",
1717
},
1818
}

build/rofl/scheduler/domain.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package scheduler
2+
3+
import (
4+
"encoding/base64"
5+
6+
"github.com/oasisprotocol/oasis-core/go/common/crypto/tuplehash"
7+
8+
"github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/rofl"
9+
"github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/roflmarket"
10+
)
11+
12+
const (
13+
/// Domain verification token context.
14+
DomainVerificationTokenContext = "rofl-scheduler/proxy: domain verification token" //nolint:gosec
15+
)
16+
17+
// DomainVerificationToken derives the verification token for a given domain.
18+
//
19+
// The token is derived using a cryptographic hash function and is used to verify
20+
// that the domain is owned by the instance. More specifically, the token is derived
21+
// as follows:
22+
//
23+
// TupleHash[DOMAIN_VERIFICATION_TOKEN_CONTEXT](
24+
// deployment.app_id,
25+
// instance.provider,
26+
// instance.id,
27+
// domain
28+
// )
29+
//
30+
// The result is then encoded using base64.
31+
func DomainVerificationToken(instance *roflmarket.Instance, appID rofl.AppID, domain string) string {
32+
h := tuplehash.New256(32, []byte(DomainVerificationTokenContext))
33+
_, _ = h.Write(appID[:])
34+
_, _ = h.Write(instance.Provider[:])
35+
_, _ = h.Write(instance.ID[:])
36+
_, _ = h.Write([]byte(domain))
37+
output := h.Sum(nil)
38+
39+
return base64.StdEncoding.EncodeToString(output)
40+
}

build/rofl/scheduler/metadata.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,6 @@ const (
1717
// MetadataKeyORCReference is the name of the deployment metadata key that stores the ORC
1818
// reference.
1919
MetadataKeyORCReference = "net.oasis.deployment.orc.ref"
20+
// MetadataKeyProxyCustomDomains is the name of the metadata key that stores the proxy custom domains.
21+
MetadataKeyProxyCustomDomains = "net.oasis.proxy.custom_domains"
2022
)

cmd/rofl/build/build.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ var (
5050

5151
if onlyValidate {
5252
fmt.Println("Validating app...")
53-
err := validateApp(manifest)
53+
_, err := ValidateApp(manifest, ValidationOpts{})
5454
if err == nil {
5555
fmt.Println("App validation passed.")
5656
return nil

cmd/rofl/build/container.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func tdxBuildContainer(
2626

2727
// Validate compose file.
2828
fmt.Println("Validating compose file...")
29-
if err := validateComposeFile(artifacts[artifactContainerCompose], manifest); err != nil {
29+
if _, err := validateComposeFile(artifacts[artifactContainerCompose], manifest, ValidationOpts{}); err != nil {
3030
common.CheckForceErr(fmt.Errorf("compose file validation failed: %w", err))
3131
}
3232

cmd/rofl/build/validate.go

Lines changed: 87 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,38 @@ import (
1717
buildRofl "github.com/oasisprotocol/cli/build/rofl"
1818
)
1919

20-
// validateApp validates the ROFL app manifest.
21-
func validateApp(manifest *buildRofl.Manifest) error {
20+
// ValidationOpts represents options for the validation process.
21+
type ValidationOpts struct {
22+
// Offline indicates whether the validation should be performed without network access.
23+
Offline bool
24+
}
25+
26+
// AppExtraConfig represents extra configuration for the ROFL app.
27+
type AppExtraConfig struct {
28+
// Ports are the port mappings exposed by the app.
29+
Ports []*PortMapping
30+
}
31+
32+
// PortMapping represents a port mapping.
33+
type PortMapping struct {
34+
// ServiceName is the name of the service.
35+
ServiceName string
36+
// Port is the port number.
37+
Port string
38+
// ProxyMode is the proxy mode for the port.
39+
ProxyMode string
40+
// GenericDomain is the generic domain name.
41+
GenericDomain string
42+
// CustomDomain is the custom domain name (if any).
43+
CustomDomain string
44+
}
45+
46+
// ValidateApp validates the ROFL app manifest.
47+
func ValidateApp(manifest *buildRofl.Manifest, opts ValidationOpts) (*AppExtraConfig, error) {
2248
switch manifest.TEE {
2349
case buildRofl.TEETypeSGX:
2450
if manifest.Kind != buildRofl.AppKindRaw {
25-
return fmt.Errorf("unsupported app kind for SGX TEE: %s", manifest.Kind)
51+
return nil, fmt.Errorf("unsupported app kind for SGX TEE: %s", manifest.Kind)
2652
}
2753
case buildRofl.TEETypeTDX:
2854
switch manifest.Kind {
@@ -38,42 +64,45 @@ func validateApp(manifest *buildRofl.Manifest) error {
3864
}
3965
}
4066
if composeAf == nil {
41-
return fmt.Errorf("missing compose.yaml artifact")
67+
return nil, fmt.Errorf("missing compose.yaml artifact")
4268
}
4369

4470
// Only fetch the compose.yaml artifact.
4571
artifacts := tdxFetchArtifacts([]*artifact{composeAf})
4672

4773
// Validate compose.yaml.
48-
err := validateComposeFile(artifacts[artifactContainerCompose], manifest)
74+
appCfg, err := validateComposeFile(artifacts[artifactContainerCompose], manifest, opts)
4975
if err != nil {
50-
return fmt.Errorf("compose file validation failed: %w", err)
76+
return nil, fmt.Errorf("compose file validation failed: %w", err)
5177
}
78+
return appCfg, nil
5279
default:
53-
return fmt.Errorf("unsupported app kind for TDX TEE: %s", manifest.Kind)
80+
return nil, fmt.Errorf("unsupported app kind for TDX TEE: %s", manifest.Kind)
5481
}
5582
default:
56-
return fmt.Errorf("unsupported TEE kind: %s", manifest.TEE)
83+
return nil, fmt.Errorf("unsupported TEE kind: %s", manifest.TEE)
5784
}
5885

59-
return nil
86+
return &AppExtraConfig{}, nil
6087
}
6188

6289
// validateComposeFile validates the Docker compose file.
63-
func validateComposeFile(composeFile string, manifest *buildRofl.Manifest) error { //nolint: gocyclo
90+
func validateComposeFile(composeFile string, manifest *buildRofl.Manifest, opts ValidationOpts) (*AppExtraConfig, error) { //nolint: gocyclo
6491
// Parse the compose file.
6592
options, err := compose.NewProjectOptions([]string{composeFile}, compose.WithInterpolation(false))
6693
if err != nil {
67-
return fmt.Errorf("failed to set-up compose options: %w", err)
94+
return nil, fmt.Errorf("failed to set-up compose options: %w", err)
6895
}
6996
proj, err := options.LoadProject(context.Background())
7097
if err != nil {
71-
return fmt.Errorf("parsing: %w", err)
98+
return nil, fmt.Errorf("parsing: %w", err)
7299
}
73100

74101
// Keep track of all images encountered, as we will need them in later steps.
75102
images := []string{}
103+
customDomains := make(map[string]struct{})
76104

105+
var appCfg AppExtraConfig
77106
for serviceName, service := range proj.Services {
78107
image := service.Image
79108
images = append(images, image)
@@ -83,21 +112,21 @@ func validateComposeFile(composeFile string, manifest *buildRofl.Manifest) error
83112
validationFailedErr := fmt.Errorf("image '%s' of service '%s' is not a fully-qualified domain name", image, serviceName)
84113

85114
if !strings.Contains(image, "/") {
86-
return validationFailedErr
115+
return nil, validationFailedErr
87116
}
88117
s := strings.Split(image, "/")
89118
if len(s[0]) == 0 || len(s[1]) == 0 {
90-
return validationFailedErr
119+
return nil, validationFailedErr
91120
}
92121

93122
domain := s[0]
94123
if !strings.Contains(domain, ".") {
95-
return validationFailedErr
124+
return nil, validationFailedErr
96125
}
97126

98127
_, err := idna.Lookup.ToASCII(domain)
99128
if err != nil {
100-
return validationFailedErr
129+
return nil, validationFailedErr
101130
}
102131

103132
// Also, if any volumes are set, make sure that the source of each volume
@@ -132,23 +161,46 @@ func validateComposeFile(composeFile string, manifest *buildRofl.Manifest) error
132161
}
133162
}
134163

135-
return fmt.Errorf("volume '%s:%s' of service '%s' has an invalid external source (should be '/run/rofl-appd.sock', '/run/podman.sock' or reside inside '/storage/')", vol.Source, vol.Target, serviceName)
164+
return nil, fmt.Errorf("volume '%s:%s' of service '%s' has an invalid external source (should be '/run/rofl-appd.sock', '/run/podman.sock' or reside inside '/storage/')", vol.Source, vol.Target, serviceName)
136165
}
137166

138167
// Validate ports.
139168
publishedPorts := make(map[uint64]struct{})
140169
for _, port := range service.Ports {
141170
if port.Target == 0 {
142-
return fmt.Errorf("service '%s' has an invalid zero port defined", serviceName)
171+
return nil, fmt.Errorf("service '%s' has an invalid zero port defined", serviceName)
143172
}
144173
if port.Published == "" || port.Published == "0" {
145-
return fmt.Errorf("port '%d' of service '%s' does not have an explicit published port defined", port.Target, serviceName)
174+
return nil, fmt.Errorf("port '%d' of service '%s' does not have an explicit published port defined", port.Target, serviceName)
146175
}
147176
publishedPort, err := strconv.ParseUint(port.Published, 10, 16)
148177
if err != nil {
149-
return fmt.Errorf("published port '%s' of service '%s' is invalid: %w", port.Published, serviceName, err)
178+
return nil, fmt.Errorf("published port '%s' of service '%s' is invalid: %w", port.Published, serviceName, err)
150179
}
151180
publishedPorts[publishedPort] = struct{}{}
181+
182+
if port.Protocol != "tcp" {
183+
continue
184+
}
185+
proxyMode := service.Annotations[fmt.Sprintf("net.oasis.proxy.ports.%s.mode", port.Published)]
186+
if proxyMode == "ignore" {
187+
continue
188+
}
189+
genericDomain := fmt.Sprintf("p%s", port.Published)
190+
customDomain := service.Annotations[fmt.Sprintf("net.oasis.proxy.ports.%s.custom_domain", port.Published)]
191+
if customDomain != "" {
192+
if _, ok := customDomains[customDomain]; ok {
193+
return nil, fmt.Errorf("custom domain '%s' of service '%s' is already in use", customDomain, serviceName)
194+
}
195+
customDomains[customDomain] = struct{}{}
196+
}
197+
appCfg.Ports = append(appCfg.Ports, &PortMapping{
198+
ServiceName: serviceName,
199+
Port: port.Published,
200+
ProxyMode: proxyMode,
201+
GenericDomain: genericDomain,
202+
CustomDomain: customDomain,
203+
})
152204
}
153205

154206
// Validate annotations.
@@ -158,10 +210,10 @@ func validateComposeFile(composeFile string, manifest *buildRofl.Manifest) error
158210
case strings.HasPrefix(key, "net.oasis.proxy.ports"):
159211
port, err := strconv.ParseUint(keyAtoms[4], 10, 16)
160212
if err != nil {
161-
return fmt.Errorf("proxy port annotation of service '%s' has an invalid port: %s", serviceName, keyAtoms[4])
213+
return nil, fmt.Errorf("proxy port annotation of service '%s' has an invalid port: %s", serviceName, keyAtoms[4])
162214
}
163215
if _, ok := publishedPorts[port]; !ok {
164-
return fmt.Errorf("proxy port annotation of service '%s' references an unpublished port '%d'", serviceName, port)
216+
return nil, fmt.Errorf("proxy port annotation of service '%s' references an unpublished port '%d'", serviceName, port)
165217
}
166218

167219
// Validate supported annotations.
@@ -173,8 +225,9 @@ func validateComposeFile(composeFile string, manifest *buildRofl.Manifest) error
173225
case "passthrough":
174226
case "terminate-tls":
175227
default:
176-
return fmt.Errorf("proxy port annotation of service '%s', port '%d' has an invalid mode: %s", serviceName, port, value)
228+
return nil, fmt.Errorf("proxy port annotation of service '%s', port '%d' has an invalid mode: %s", serviceName, port, value)
177229
}
230+
case "custom_domain":
178231
default:
179232
}
180233
default:
@@ -184,7 +237,7 @@ func validateComposeFile(composeFile string, manifest *buildRofl.Manifest) error
184237

185238
// Make sure that we have images.
186239
if len(images) == 0 {
187-
return fmt.Errorf("no images defined")
240+
return nil, fmt.Errorf("no images defined")
188241
}
189242

190243
// Make sure that the total size of images fits into the storage size
@@ -214,25 +267,29 @@ func validateComposeFile(composeFile string, manifest *buildRofl.Manifest) error
214267
}
215268
}
216269

270+
if opts.Offline {
271+
continue
272+
}
273+
217274
// Fetch manifest from OCI repository.
218275
repo, err := remote.NewRepository(image)
219276
if err != nil {
220-
return err
277+
return nil, err
221278
}
222279
mainDescriptor, mfRaw, err := oras.FetchBytes(context.Background(), repo, tag, oras.DefaultFetchBytesOptions)
223280
if err != nil {
224-
return fmt.Errorf("unable to fetch manifest for image '%s': %w", imageFull, err)
281+
return nil, fmt.Errorf("unable to fetch manifest for image '%s': %w", imageFull, err)
225282
}
226283
var mf ocispec.Manifest
227284
if err = json.Unmarshal(mfRaw, &mf); err != nil {
228-
return fmt.Errorf("unable to parse manifest for image '%s': %w", imageFull, err)
285+
return nil, fmt.Errorf("unable to parse manifest for image '%s': %w", imageFull, err)
229286
}
230287

231288
// Validate platform if given.
232289
for _, platform := range []*ocispec.Platform{mainDescriptor.Platform, mf.Config.Platform} {
233290
if platform != nil {
234291
if platform.Architecture != "amd64" || platform.OS != "linux" {
235-
return fmt.Errorf("image '%s' has incorrect platform (expected linux/amd64, got %s/%s)", imageFull, platform.OS, platform.Architecture)
292+
return nil, fmt.Errorf("image '%s' has incorrect platform (expected linux/amd64, got %s/%s)", imageFull, platform.OS, platform.Architecture)
236293
}
237294
}
238295
}
@@ -254,8 +311,8 @@ func validateComposeFile(composeFile string, manifest *buildRofl.Manifest) error
254311
// We could terminate early above, but it's more useful to give the user an estimate
255312
// of how big the storage should be.
256313
if totalSize > maxSize {
257-
return fmt.Errorf("estimated total size of images (%d MB) exceeds storage size set in ROFL manifest (%d MB)", totalSize/1024/1024, manifest.Resources.Storage.Size)
314+
return nil, fmt.Errorf("estimated total size of images (%d MB) exceeds storage size set in ROFL manifest (%d MB)", totalSize/1024/1024, manifest.Resources.Storage.Size)
258315
}
259316

260-
return nil
317+
return &appCfg, nil
261318
}

cmd/rofl/deploy.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"github.com/oasisprotocol/cli/build/rofl/provider"
3131
"github.com/oasisprotocol/cli/build/rofl/scheduler"
3232
"github.com/oasisprotocol/cli/cmd/common"
33+
roflCmdBuild "github.com/oasisprotocol/cli/cmd/rofl/build"
3334
roflCommon "github.com/oasisprotocol/cli/cmd/rofl/common"
3435
cliConfig "github.com/oasisprotocol/cli/config"
3536
)
@@ -63,6 +64,13 @@ var (
6364
cobra.CheckErr(fmt.Sprintf("malformed app id: %s", err))
6465
}
6566

67+
extraCfg, err := roflCmdBuild.ValidateApp(manifest, roflCmdBuild.ValidationOpts{
68+
Offline: true,
69+
})
70+
if err != nil {
71+
cobra.CheckErr(fmt.Sprintf("failed to validate app: %s", err))
72+
}
73+
6674
ctx := context.Background()
6775
conn, err := connection.Connect(ctx, npa.Network)
6876
cobra.CheckErr(err)
@@ -154,6 +162,9 @@ var (
154162
}
155163
machineDeployment.Metadata[scheduler.MetadataKeyPermissions] = perms
156164
}
165+
if customDomains := extractCustomDomains(extraCfg); customDomains != "" {
166+
machineDeployment.Metadata[scheduler.MetadataKeyProxyCustomDomains] = customDomains
167+
}
157168

158169
obtainMachine := func() (*buildRofl.Machine, *roflmarket.Instance, error) {
159170
if deployOffer != "" {
@@ -481,6 +492,21 @@ func resolveAndMarshalPermissions(npa *common.NPASelection, permissions map[stri
481492
return scheduler.MarshalPermissions(perms), nil
482493
}
483494

495+
func extractCustomDomains(extraCfg *roflCmdBuild.AppExtraConfig) string {
496+
if extraCfg == nil || len(extraCfg.Ports) == 0 {
497+
return ""
498+
}
499+
500+
customDomains := make([]string, 0, len(extraCfg.Ports))
501+
for _, p := range extraCfg.Ports {
502+
if p.CustomDomain == "" {
503+
continue
504+
}
505+
customDomains = append(customDomains, p.CustomDomain)
506+
}
507+
return strings.Join(customDomains, " ")
508+
}
509+
484510
func init() {
485511
providerFlags := flag.NewFlagSet("", flag.ContinueOnError)
486512
// Default to Testnet playground provider.

0 commit comments

Comments
 (0)