diff --git a/build/rofl/manifest.go b/build/rofl/manifest.go index 14d1f8ce..d9b7459a 100644 --- a/build/rofl/manifest.go +++ b/build/rofl/manifest.go @@ -231,6 +231,21 @@ func (m *Manifest) GetMetadata(deployment string) map[string]string { return meta } +// ResolveArtifacts resolves the artifact configuration for the given deployment by overlaying +// global manifest artifacts and then deployment-specific artifacts on top of the provided defaults. +func (m *Manifest) ResolveArtifacts(deployment string, defaults ArtifactsConfig) ArtifactsConfig { + resolved := defaults + resolved.Merge(m.Artifacts) + + if deployment != "" { + if d := m.Deployments[deployment]; d != nil { + resolved.Merge(d.Artifacts) + } + } + + return resolved +} + // SourceFileName returns the filename of the manifest file from which the manifest was loaded or // an empty string in case the filename is not available. func (m *Manifest) SourceFileName() string { @@ -319,6 +334,8 @@ type Deployment struct { Debug bool `yaml:"debug,omitempty" json:"debug,omitempty"` // OCIRepository is the optional OCI repository where one can push the ORC to. OCIRepository string `yaml:"oci_repository,omitempty" json:"oci_repository,omitempty"` + // Artifacts are optional deployment-specific artifact location overrides. + Artifacts *ArtifactsConfig `yaml:"artifacts,omitempty" json:"artifacts,omitempty"` // TrustRoot is the optional trust root configuration. TrustRoot *TrustRootConfig `yaml:"trust_root,omitempty" json:"trust_root,omitempty"` // Policy is the ROFL app policy. @@ -564,6 +581,26 @@ type ArtifactsConfig struct { Container ContainerArtifactsConfig `yaml:"container,omitempty" json:"container,omitempty"` } +// Merge overlays non-empty artifact fields from another artifact configuration. +func (ac *ArtifactsConfig) Merge(other *ArtifactsConfig) { + if other == nil { + return + } + if other.Builder != "" { + ac.Builder = other.Builder + } + if other.Firmware != "" { + ac.Firmware = other.Firmware + } + if other.Kernel != "" { + ac.Kernel = other.Kernel + } + if other.Stage2 != "" { + ac.Stage2 = other.Stage2 + } + ac.Container.Merge(&other.Container) +} + type artifactUpgrade struct { existing *string new string @@ -599,6 +636,22 @@ func upgradePossible(check []artifactUpgrade) bool { return false } +// upgradeExplicitArtifacts upgrades only explicitly configured artifact fields. +func upgradeExplicitArtifacts(upgrade []artifactUpgrade) bool { + var changed bool + for _, artifact := range upgrade { + if artifact.new == "" || *artifact.existing == "" { + continue + } + if *artifact.existing == artifact.new { + continue + } + *artifact.existing = artifact.new + changed = true + } + return changed +} + // UpgradePossible returns true iff any explicitly set artifacts differ from latest. // Empty fields are ignored (they use defaults from code, so already latest). func (ac *ArtifactsConfig) UpgradePossible(latest *ArtifactsConfig) bool { @@ -636,6 +689,21 @@ func (ac *ArtifactsConfig) UpgradeTo(latest *ArtifactsConfig) bool { return changed } +// UpgradeExplicitTo upgrades only explicitly configured artifacts to the latest version. +// +// Returns true iff any artifacts have been updated. +func (ac *ArtifactsConfig) UpgradeExplicitTo(latest *ArtifactsConfig) bool { + var changed bool + changed = upgradeExplicitArtifacts([]artifactUpgrade{ + {&ac.Builder, latest.Builder}, + {&ac.Firmware, latest.Firmware}, + {&ac.Kernel, latest.Kernel}, + {&ac.Stage2, latest.Stage2}, + }) + changed = ac.Container.UpgradeExplicitTo(&latest.Container) || changed + return changed +} + // ContainerArtifactsConfig is the container artifacts configuration. type ContainerArtifactsConfig struct { // Runtime is the URI/path to the container runtime artifact (empty to use default). @@ -644,6 +712,19 @@ type ContainerArtifactsConfig struct { Compose string `yaml:"compose,omitempty" json:"compose,omitempty"` } +// Merge overlays non-empty container artifact fields from another container artifact configuration. +func (cc *ContainerArtifactsConfig) Merge(other *ContainerArtifactsConfig) { + if other == nil { + return + } + if other.Runtime != "" { + cc.Runtime = other.Runtime + } + if other.Compose != "" { + cc.Compose = other.Compose + } +} + // UpgradeTo upgrades the artifacts to the latest version by updating any relevant fields. // // Returns true iff any artifacts have been updated. @@ -653,3 +734,13 @@ func (cc *ContainerArtifactsConfig) UpgradeTo(latest *ContainerArtifactsConfig) {&cc.Runtime, latest.Runtime}, }) } + +// UpgradeExplicitTo upgrades only explicitly configured container artifacts to the latest version. +// +// Returns true iff any artifacts have been updated. +func (cc *ContainerArtifactsConfig) UpgradeExplicitTo(latest *ContainerArtifactsConfig) bool { + return upgradeExplicitArtifacts([]artifactUpgrade{ + {&cc.Compose, latest.Compose}, + {&cc.Runtime, latest.Runtime}, + }) +} diff --git a/build/rofl/manifest_test.go b/build/rofl/manifest_test.go index e4966e19..77cf24c1 100644 --- a/build/rofl/manifest_test.go +++ b/build/rofl/manifest_test.go @@ -190,6 +190,133 @@ func TestManifestSerialization(t *testing.T) { require.NoError(err, "dec.Validate") } +func TestDeploymentArtifactsSerialization(t *testing.T) { + require := require.New(t) + + const manifestYaml = ` +name: my-container-app +version: 0.1.0 +tee: tdx +kind: container +resources: + memory: 512 + cpus: 1 + storage: + kind: disk-persistent + size: 512 +artifacts: + firmware: global-firmware + kernel: global-kernel + stage2: global-stage2 + container: + runtime: global-runtime + compose: compose.yaml +deployments: + testnet: + network: testnet + paratime: sapphire + artifacts: + container: + compose: compose.testnet.yaml +` + + var m Manifest + err := yaml.Unmarshal([]byte(manifestYaml), &m) + require.NoError(err, "yaml.Unmarshal") + require.NoError(m.Validate()) + require.NotNil(m.Artifacts) + require.Equal("compose.yaml", m.Artifacts.Container.Compose) + require.NotNil(m.Deployments["testnet"].Artifacts) + require.Equal("compose.testnet.yaml", m.Deployments["testnet"].Artifacts.Container.Compose) + + enc, err := yaml.Marshal(m) + require.NoError(err, "yaml.Marshal") + + var dec Manifest + err = yaml.Unmarshal(enc, &dec) + require.NoError(err, "yaml.Unmarshal(round-trip)") + require.EqualValues(m, dec, "serialization should round-trip") + require.NoError(dec.Validate()) +} + +func TestResolveArtifacts(t *testing.T) { + require := require.New(t) + + const ( + defaultFirmware = "default-firmware" + defaultStage2 = "default-stage2" + defaultRuntime = "default-runtime" + globalKernel = "global-kernel" + globalCompose = "global-compose" + deploymentStage2 = "deployment-stage2" + deploymentCompose = "deployment-compose" + ) + + defaults := ArtifactsConfig{ + Firmware: defaultFirmware, + Kernel: "default-kernel", + Stage2: defaultStage2, + Container: ContainerArtifactsConfig{ + Runtime: defaultRuntime, + Compose: "default-compose", + }, + } + m := Manifest{ + Artifacts: &ArtifactsConfig{ + Kernel: globalKernel, + Container: ContainerArtifactsConfig{ + Compose: globalCompose, + }, + }, + Deployments: map[string]*Deployment{ + "testnet": { + Network: "testnet", + ParaTime: "sapphire", + Artifacts: &ArtifactsConfig{ + Stage2: deploymentStage2, + Container: ContainerArtifactsConfig{ + Compose: deploymentCompose, + }, + }, + }, + "mainnet": { + Network: "mainnet", + ParaTime: "sapphire", + }, + }, + } + + require.Equal(ArtifactsConfig{ + Firmware: defaultFirmware, + Kernel: globalKernel, + Stage2: deploymentStage2, + Container: ContainerArtifactsConfig{ + Runtime: defaultRuntime, + Compose: deploymentCompose, + }, + }, m.ResolveArtifacts("testnet", defaults)) + + require.Equal(ArtifactsConfig{ + Firmware: defaultFirmware, + Kernel: globalKernel, + Stage2: defaultStage2, + Container: ContainerArtifactsConfig{ + Runtime: defaultRuntime, + Compose: globalCompose, + }, + }, m.ResolveArtifacts("mainnet", defaults)) + + require.Equal(ArtifactsConfig{ + Firmware: defaultFirmware, + Kernel: globalKernel, + Stage2: defaultStage2, + Container: ContainerArtifactsConfig{ + Runtime: defaultRuntime, + Compose: globalCompose, + }, + }, m.ResolveArtifacts("missing", defaults)) +} + func TestLoadManifest(t *testing.T) { require := require.New(t) @@ -264,6 +391,36 @@ func TestUpgradeArtifacts(t *testing.T) { require.False(changed) } +func TestUpgradeExplicitArtifacts(t *testing.T) { + require := require.New(t) + + existing := ArtifactsConfig{ + Kernel: "old-kernel", + Container: ContainerArtifactsConfig{ + Compose: "compose.testnet.yaml", + }, + } + latest := ArtifactsConfig{ + Firmware: "latest-firmware", + Kernel: "latest-kernel", + Stage2: "latest-stage2", + Container: ContainerArtifactsConfig{ + Runtime: "latest-runtime", + }, + } + + changed := existing.UpgradeExplicitTo(&latest) + require.True(changed) + require.Equal("", existing.Firmware) + require.Equal("latest-kernel", existing.Kernel) + require.Equal("", existing.Stage2) + require.Equal("", existing.Container.Runtime) + require.Equal("compose.testnet.yaml", existing.Container.Compose) + + changed = existing.UpgradeExplicitTo(&latest) + require.False(changed) +} + func TestUpgradePossible(t *testing.T) { require := require.New(t) diff --git a/cmd/rofl/build/build.go b/cmd/rofl/build/build.go index f434cefd..451c1945 100644 --- a/cmd/rofl/build/build.go +++ b/cmd/rofl/build/build.go @@ -54,7 +54,7 @@ var ( if onlyValidate { fmt.Println("Validating app...") - _, err := ValidateApp(manifest, ValidationOpts{}) + _, err := ValidateApp(manifest, roflCommon.DeploymentName, ValidationOpts{}) if err == nil { fmt.Println("App validation passed.") return nil @@ -86,10 +86,11 @@ var ( setUmask(0o002) // Determine builder image to use. + builderArtifacts := manifest.ResolveArtifacts(roflCommon.DeploymentName, buildRofl.ArtifactsConfig{}) builderImage := "" - if manifest.Artifacts != nil { - builderImage = strings.TrimSpace(manifest.Artifacts.Builder) - if manifest.Artifacts.Builder != "" && builderImage == "" { + if builderArtifacts.Builder != "" { + builderImage = strings.TrimSpace(builderArtifacts.Builder) + if builderImage == "" { return fmt.Errorf("builder image is empty after trimming whitespace") } } @@ -191,9 +192,11 @@ var ( // TDX. switch manifest.Kind { case buildRofl.AppKindRaw: - err = tdxBuildRaw(buildEnv, tmpDir, npa, manifest, deployment, bnd, doVerify) + artifactsCfg := manifest.ResolveArtifacts(roflCommon.DeploymentName, buildRofl.LatestBasicArtifacts) + err = tdxBuildRaw(buildEnv, tmpDir, npa, manifest, deployment, artifactsCfg, bnd, doVerify) case buildRofl.AppKindContainer: - err = tdxBuildContainer(buildEnv, tmpDir, npa, manifest, deployment, bnd) + artifactsCfg := manifest.ResolveArtifacts(roflCommon.DeploymentName, buildRofl.LatestContainerArtifacts) + err = tdxBuildContainer(buildEnv, tmpDir, npa, manifest, deployment, artifactsCfg, bnd) } default: return fmt.Errorf("unsupported TEE kind: %s", manifest.TEE) @@ -291,7 +294,7 @@ var ( } // Check if artifact upgrades are available and notify the user. - notifyUpgradeAvailable(manifest) + notifyUpgradeAvailable(manifest, roflCommon.DeploymentName) return nil } @@ -338,7 +341,7 @@ var ( } // Check if artifact upgrades are available and notify the user. - notifyUpgradeAvailable(manifest) + notifyUpgradeAvailable(manifest, roflCommon.DeploymentName) return nil }, @@ -346,35 +349,36 @@ var ( ) // notifyUpgradeAvailable checks if artifact upgrades are available and prints a notification. -func notifyUpgradeAvailable(manifest *buildRofl.Manifest) { - var latestArtifacts buildRofl.ArtifactsConfig - switch manifest.TEE { - case buildRofl.TEETypeTDX: - switch manifest.Kind { - case buildRofl.AppKindRaw: - latestArtifacts = buildRofl.LatestBasicArtifacts - latestArtifacts.Builder = buildRofl.LatestBuilderImage - case buildRofl.AppKindContainer: - latestArtifacts = buildRofl.LatestContainerArtifacts - latestArtifacts.Builder = buildRofl.LatestContainerBuilderImage - default: - return - } - default: +func notifyUpgradeAvailable(manifest *buildRofl.Manifest, deploymentName string) { + latestArtifacts, ok := latestUpgradeableArtifacts(manifest) + if !ok { return } - current := manifest.Artifacts - if current == nil { - current = &buildRofl.ArtifactsConfig{} - } - + current := manifest.ResolveArtifacts(deploymentName, buildRofl.ArtifactsConfig{}) if current.UpgradePossible(&latestArtifacts) { fmt.Println() fmt.Println("NOTE: A new version of artifacts is available. Run `oasis rofl upgrade` to upgrade.") } } +func latestUpgradeableArtifacts(manifest *buildRofl.Manifest) (buildRofl.ArtifactsConfig, bool) { + if manifest.TEE != buildRofl.TEETypeTDX { + return buildRofl.ArtifactsConfig{}, false + } + switch manifest.Kind { + case buildRofl.AppKindRaw: + latestArtifacts := buildRofl.LatestBasicArtifacts + latestArtifacts.Builder = buildRofl.LatestBuilderImage + return latestArtifacts, true + case buildRofl.AppKindContainer: + latestArtifacts := buildRofl.LatestContainerArtifacts + latestArtifacts.Builder = buildRofl.LatestContainerBuilderImage + return latestArtifacts, true + } + return buildRofl.ArtifactsConfig{}, false +} + // setupContainerEnv creates and initializes a container build environment. func setupContainerEnv(builderImage string) (env.ExecEnv, error) { baseDir, err := env.GetBasedir() diff --git a/cmd/rofl/build/container.go b/cmd/rofl/build/container.go index 6cb41f56..0ee3c1e5 100644 --- a/cmd/rofl/build/container.go +++ b/cmd/rofl/build/container.go @@ -17,16 +17,21 @@ func tdxBuildContainer( npa *common.NPASelection, manifest *buildRofl.Manifest, deployment *buildRofl.Deployment, + artifactsCfg buildRofl.ArtifactsConfig, bnd *bundle.Bundle, ) error { fmt.Println("Building a container-based TDX ROFL application...") - wantedArtifacts := tdxWantedArtifacts(manifest, buildRofl.LatestContainerArtifacts) + wantedArtifacts := tdxWantedArtifacts(artifactsCfg) artifacts := tdxFetchArtifacts(wantedArtifacts) + composePath, ok := artifacts[artifactContainerCompose] + if !ok { + return fmt.Errorf("missing compose.yaml artifact") + } // Validate compose file. fmt.Println("Validating compose file...") - if _, err := validateComposeFile(artifacts[artifactContainerCompose], manifest, ValidationOpts{}); err != nil { + if _, err := validateComposeFile(composePath, manifest, ValidationOpts{}); err != nil { common.CheckForceErr(fmt.Errorf("compose file validation failed: %w", err)) } @@ -34,7 +39,7 @@ func tdxBuildContainer( initPath := artifacts[artifactContainerRuntime] stage2, err := tdxPrepareStage2(buildEnv, tmpDir, artifacts, initPath, []extraFile{ - {HostPath: artifacts[artifactContainerCompose], TarPath: "etc/oasis/containers/compose.yaml", Mode: 0o644}, + {HostPath: composePath, TarPath: "etc/oasis/containers/compose.yaml", Mode: 0o644}, }) if err != nil { return err diff --git a/cmd/rofl/build/tdx.go b/cmd/rofl/build/tdx.go index b48f6dca..2d281e2b 100644 --- a/cmd/rofl/build/tdx.go +++ b/cmd/rofl/build/tdx.go @@ -35,10 +35,11 @@ func tdxBuildRaw( npa *common.NPASelection, manifest *buildRofl.Manifest, deployment *buildRofl.Deployment, + artifactsCfg buildRofl.ArtifactsConfig, bnd *bundle.Bundle, locked bool, ) error { - wantedArtifacts := tdxWantedArtifacts(manifest, buildRofl.LatestBasicArtifacts) + wantedArtifacts := tdxWantedArtifacts(artifactsCfg) artifacts := tdxFetchArtifacts(wantedArtifacts) fmt.Println("Building a TDX-based Rust ROFL application...") @@ -83,9 +84,8 @@ type artifact struct { uri string } -// tdxWantedArtifacts returns the list of wanted artifacts based on the passed manifest and a set of -// defaults. In case an artifact is not defined in the manifest, it is taken from defaults. -func tdxWantedArtifacts(manifest *buildRofl.Manifest, defaults buildRofl.ArtifactsConfig) []*artifact { +// tdxWantedArtifacts returns the list of wanted artifacts based on resolved artifact config. +func tdxWantedArtifacts(artifactsCfg buildRofl.ArtifactsConfig) []*artifact { var artifacts []*artifact for _, a := range []struct { kind string @@ -97,13 +97,7 @@ func tdxWantedArtifacts(manifest *buildRofl.Manifest, defaults buildRofl.Artifac {artifactContainerRuntime, func(ac *buildRofl.ArtifactsConfig) string { return ac.Container.Runtime }}, {artifactContainerCompose, func(ac *buildRofl.ArtifactsConfig) string { return ac.Container.Compose }}, } { - var uri string - if manifest.Artifacts != nil { - uri = a.getter(manifest.Artifacts) - } - if uri == "" { - uri = a.getter(&defaults) - } + uri := a.getter(&artifactsCfg) if uri != "" { artifacts = append(artifacts, &artifact{a.kind, uri}) } diff --git a/cmd/rofl/build/tdx_test.go b/cmd/rofl/build/tdx_test.go new file mode 100644 index 00000000..9ebe2fc4 --- /dev/null +++ b/cmd/rofl/build/tdx_test.go @@ -0,0 +1,133 @@ +package build + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + buildRofl "github.com/oasisprotocol/cli/build/rofl" +) + +const ( + globalComposeFile = "compose.yaml" + testnetComposeFile = "compose.testnet.yaml" +) + +func TestTdxWantedArtifactsUsesResolvedConfig(t *testing.T) { + artifacts := tdxWantedArtifacts(buildRofl.ArtifactsConfig{ + Firmware: "firmware", + Kernel: "kernel", + Stage2: "stage2", + Container: buildRofl.ContainerArtifactsConfig{ + Runtime: "runtime", + Compose: "compose", + }, + }) + + got := make(map[string]string) + for _, artifact := range artifacts { + got[artifact.kind] = artifact.uri + } + + require.Equal(t, map[string]string{ + artifactFirmware: "firmware", + artifactKernel: "kernel", + artifactStage2: "stage2", + artifactContainerRuntime: "runtime", + artifactContainerCompose: "compose", + }, got) +} + +func TestValidateAppUsesDeploymentComposeOverride(t *testing.T) { + tmpDir := t.TempDir() + t.Chdir(tmpDir) + + writeComposeFile(t, filepath.Join(tmpDir, globalComposeFile), "8080", "global.example.com") + writeComposeFile(t, filepath.Join(tmpDir, testnetComposeFile), "9090", "testnet.example.com") + + manifest := testContainerManifest() + manifest.Artifacts = &buildRofl.ArtifactsConfig{ + Container: buildRofl.ContainerArtifactsConfig{ + Compose: globalComposeFile, + }, + } + manifest.Deployments = map[string]*buildRofl.Deployment{ + "testnet": { + Network: "testnet", + ParaTime: "sapphire", + Artifacts: &buildRofl.ArtifactsConfig{ + Container: buildRofl.ContainerArtifactsConfig{ + Compose: testnetComposeFile, + }, + }, + }, + } + + cfg, err := ValidateApp(manifest, "testnet", ValidationOpts{Offline: true}) + require.NoError(t, err) + require.Len(t, cfg.Ports, 1) + require.Equal(t, "9090", cfg.Ports[0].Port) + require.Equal(t, "testnet.example.com", cfg.Ports[0].CustomDomain) +} + +func TestValidateAppFallsBackToGlobalCompose(t *testing.T) { + tmpDir := t.TempDir() + t.Chdir(tmpDir) + + writeComposeFile(t, filepath.Join(tmpDir, globalComposeFile), "8080", "global.example.com") + + manifest := testContainerManifest() + manifest.Artifacts = &buildRofl.ArtifactsConfig{ + Container: buildRofl.ContainerArtifactsConfig{ + Compose: globalComposeFile, + }, + } + manifest.Deployments = map[string]*buildRofl.Deployment{ + "testnet": { + Network: "testnet", + ParaTime: "sapphire", + }, + } + + cfg, err := ValidateApp(manifest, "testnet", ValidationOpts{Offline: true}) + require.NoError(t, err) + require.Len(t, cfg.Ports, 1) + require.Equal(t, "8080", cfg.Ports[0].Port) + require.Equal(t, "global.example.com", cfg.Ports[0].CustomDomain) +} + +func testContainerManifest() *buildRofl.Manifest { + return &buildRofl.Manifest{ + Name: "test-app", + Version: "0.1.0", + TEE: buildRofl.TEETypeTDX, + Kind: buildRofl.AppKindContainer, + Resources: buildRofl.ResourcesConfig{ + Memory: 512, + CPUCount: 1, + Storage: &buildRofl.StorageConfig{ + Kind: buildRofl.StorageKindDiskPersistent, + Size: 512, + }, + }, + } +} + +func writeComposeFile(t *testing.T, path, publishedPort, customDomain string) { + t.Helper() + + compose := []byte(`services: + web: + image: ghcr.io/oasisprotocol/test:latest + ports: + - target: 80 + published: "` + publishedPort + `" + protocol: tcp + annotations: + "net.oasis.proxy.ports.` + publishedPort + `.mode": terminate-tls + "net.oasis.proxy.ports.` + publishedPort + `.custom_domain": ` + customDomain + ` +`) + require.NoError(t, os.WriteFile(path, compose, 0o600)) +} diff --git a/cmd/rofl/build/validate.go b/cmd/rofl/build/validate.go index d353878e..35bec99b 100644 --- a/cmd/rofl/build/validate.go +++ b/cmd/rofl/build/validate.go @@ -43,8 +43,8 @@ type PortMapping struct { CustomDomain string } -// ValidateApp validates the ROFL app manifest. -func ValidateApp(manifest *buildRofl.Manifest, opts ValidationOpts) (*AppExtraConfig, error) { +// ValidateApp validates the ROFL app manifest for the selected deployment. +func ValidateApp(manifest *buildRofl.Manifest, deploymentName string, opts ValidationOpts) (*AppExtraConfig, error) { switch manifest.TEE { case buildRofl.TEETypeSGX: if manifest.Kind != buildRofl.AppKindRaw { @@ -54,21 +54,17 @@ func ValidateApp(manifest *buildRofl.Manifest, opts ValidationOpts) (*AppExtraCo switch manifest.Kind { case buildRofl.AppKindRaw: case buildRofl.AppKindContainer: - wantedArtifacts := tdxWantedArtifacts(manifest, buildRofl.LatestContainerArtifacts) - - var composeAf *artifact - for _, a := range wantedArtifacts { - if a.kind == artifactContainerCompose { - composeAf = a - break + artifactsCfg := manifest.ResolveArtifacts(deploymentName, buildRofl.LatestContainerArtifacts) + composeURI := artifactsCfg.Container.Compose + if composeURI == "" { + if deploymentName != "" { + return nil, fmt.Errorf("missing compose.yaml artifact for deployment '%s'", deploymentName) } - } - if composeAf == nil { return nil, fmt.Errorf("missing compose.yaml artifact") } // Only fetch the compose.yaml artifact. - artifacts := tdxFetchArtifacts([]*artifact{composeAf}) + artifacts := tdxFetchArtifacts([]*artifact{{kind: artifactContainerCompose, uri: composeURI}}) // Validate compose.yaml. appCfg, err := validateComposeFile(artifacts[artifactContainerCompose], manifest, opts) diff --git a/cmd/rofl/deploy.go b/cmd/rofl/deploy.go index ba7ec1cb..632d38be 100644 --- a/cmd/rofl/deploy.go +++ b/cmd/rofl/deploy.go @@ -64,7 +64,7 @@ var ( cobra.CheckErr(fmt.Sprintf("malformed app id: %s", err)) } - extraCfg, err := roflCmdBuild.ValidateApp(manifest, roflCmdBuild.ValidationOpts{ + extraCfg, err := roflCmdBuild.ValidateApp(manifest, roflCommon.DeploymentName, roflCmdBuild.ValidationOpts{ Offline: true, }) if err != nil { diff --git a/cmd/rofl/machine/mgmt.go b/cmd/rofl/machine/mgmt.go index 3dfd57a4..f80cf3d1 100644 --- a/cmd/rofl/machine/mgmt.go +++ b/cmd/rofl/machine/mgmt.go @@ -324,7 +324,7 @@ func resolveMachineCfg(args []string, manifestOpts *roflCommon.ManifestOptions) return nil, fmt.Errorf("malformed app id: %w", err) } - if mCfg.ExtraCfg, err = roflCmdBuild.ValidateApp(mCfg.Manifest, roflCmdBuild.ValidationOpts{ + if mCfg.ExtraCfg, err = roflCmdBuild.ValidateApp(mCfg.Manifest, roflCommon.DeploymentName, roflCmdBuild.ValidationOpts{ Offline: true, }); err != nil { return nil, fmt.Errorf("failed to validate app: %w", err) diff --git a/cmd/rofl/mgmt.go b/cmd/rofl/mgmt.go index e23a5053..60114f34 100644 --- a/cmd/rofl/mgmt.go +++ b/cmd/rofl/mgmt.go @@ -582,8 +582,7 @@ var ( cobra.CheckErr(err) var latestArtifacts buildRofl.ArtifactsConfig - switch manifest.TEE { - case buildRofl.TEETypeTDX: + if manifest.TEE == buildRofl.TEETypeTDX { switch manifest.Kind { case buildRofl.AppKindRaw: latestArtifacts = buildRofl.LatestBasicArtifacts // Copy. @@ -591,15 +590,24 @@ var ( case buildRofl.AppKindContainer: latestArtifacts = buildRofl.LatestContainerArtifacts // Copy. latestArtifacts.Builder = buildRofl.LatestContainerBuilderImage - default: } - default: } if manifest.Artifacts == nil { manifest.Artifacts = &buildRofl.ArtifactsConfig{} } artifactsUpdated := manifest.Artifacts.UpgradeTo(&latestArtifacts) + var deploymentArtifactsUpdated []string + for name, deployment := range manifest.Deployments { + if deployment == nil || deployment.Artifacts == nil { + continue + } + if deployment.Artifacts.UpgradeExplicitTo(&latestArtifacts) { + deploymentArtifactsUpdated = append(deploymentArtifactsUpdated, name) + artifactsUpdated = true + } + } + sort.Strings(deploymentArtifactsUpdated) // Update tooling version. manifest.Tooling = &buildRofl.ToolingConfig{ @@ -613,6 +621,9 @@ var ( if artifactsUpdated { fmt.Printf("Artifacts have been updated to the latest versions.\n") + if len(deploymentArtifactsUpdated) > 0 { + fmt.Printf("Updated deployment artifact overrides: %s.\n", strings.Join(deploymentArtifactsUpdated, ", ")) + } fmt.Printf("Run `oasis rofl build` to build with the new artifacts.\n") } else { fmt.Printf("Artifacts already up-to-date.\n") diff --git a/cmd/rofl/mgmt_test.go b/cmd/rofl/mgmt_test.go new file mode 100644 index 00000000..48d3fa9c --- /dev/null +++ b/cmd/rofl/mgmt_test.go @@ -0,0 +1,110 @@ +package rofl + +import ( + "bytes" + "io" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + buildRofl "github.com/oasisprotocol/cli/build/rofl" +) + +func TestUpgradeUpdatesDeploymentArtifacts(t *testing.T) { + require := require.New(t) + + tmpDir := t.TempDir() + t.Chdir(tmpDir) + + const manifestYAML = ` +name: upgrade-test +version: 0.1.0 +tee: tdx +kind: container +resources: + memory: 512 + cpus: 1 + storage: + kind: disk-persistent + size: 512 +artifacts: + builder: old-builder + firmware: old-firmware + kernel: old-kernel + stage2: old-stage2 + container: + runtime: old-runtime + compose: compose.yaml +deployments: + testnet: + network: testnet + paratime: sapphire + artifacts: + kernel: old-testnet-kernel + container: + compose: compose.testnet.yaml + staging: + network: testnet + paratime: sapphire + artifacts: + kernel: old-staging-kernel + container: + compose: compose.staging.yaml +` + err := os.WriteFile(filepath.Join(tmpDir, "rofl.yaml"), []byte(manifestYAML), 0o600) + require.NoError(err) + + output := captureStdout(t, func() { + upgradeCmd.Run(upgradeCmd, nil) + }) + + updated, err := buildRofl.LoadManifest() + require.NoError(err) + + latest := buildRofl.LatestContainerArtifacts + latest.Builder = buildRofl.LatestContainerBuilderImage + + require.Equal(latest.Builder, updated.Artifacts.Builder) + require.Equal(latest.Firmware, updated.Artifacts.Firmware) + require.Equal(latest.Kernel, updated.Artifacts.Kernel) + require.Equal(latest.Stage2, updated.Artifacts.Stage2) + require.Equal(latest.Container.Runtime, updated.Artifacts.Container.Runtime) + require.Equal("compose.yaml", updated.Artifacts.Container.Compose) + + for _, deploymentName := range []string{"staging", "testnet"} { + artifacts := updated.Deployments[deploymentName].Artifacts + require.NotNil(artifacts) + require.Equal(latest.Kernel, artifacts.Kernel) + require.Empty(artifacts.Firmware) + require.Empty(artifacts.Stage2) + require.Empty(artifacts.Container.Runtime) + require.Equal("compose."+deploymentName+".yaml", artifacts.Container.Compose) + } + + require.Contains(output, "Artifacts have been updated to the latest versions.") + require.Contains(output, "Updated deployment artifact overrides: staging, testnet.") + require.Contains(output, "Run `oasis rofl build` to build with the new artifacts.") +} + +func captureStdout(t *testing.T, fn func()) string { + t.Helper() + + origStdout := os.Stdout + defer func() { + os.Stdout = origStdout + }() + + r, w, err := os.Pipe() + require.NoError(t, err) + os.Stdout = w + + fn() + + require.NoError(t, w.Close()) + var buf bytes.Buffer + _, err = io.Copy(&buf, r) + require.NoError(t, err) + return buf.String() +}