Skip to content
Merged
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
14 changes: 14 additions & 0 deletions docs/project-specification/01-authoring-projects.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,20 @@ x-topo:
example: string # Optional
```

### Deployment success message

`deployment_success_message` supports standard Compose interpolation. Topo provides these additional variables when it displays the message:

- `TOPO_TARGET`: The complete SSH destination in URI form, such as `ssh://user@board.local`.
- `TOPO_TARGET_HOSTNAME`: The Target hostname resolved from the SSH configuration, such as `board.local`.

```yaml
x-topo:
name: "web-app"
deployment_success_message: |
Open http://${TOPO_TARGET_HOSTNAME:-localhost}:8080
```

---

## 4. Testing Your Project
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
},
"deployment_success_message": {
"type": "string",
"description": "Message displayed to the user after a successful deployment. If omitted, a default message is shown."
"description": "Message displayed after a successful deployment. Supports Compose interpolation. Topo provides TOPO_TARGET and TOPO_TARGET_HOSTNAME when rendering this message. If omitted, a default message is shown."
},
"parameters": {
"type": "object",
Expand Down
9 changes: 9 additions & 0 deletions internal/compose/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,21 @@ func PullableServices(composeFilePath string) ([]string, error) {
}

func ReadProject(targetProjectFile string) (*types.Project, error) {
return readProject(targetProjectFile, nil)
}

func ReadProjectWithEnv(targetProjectFile string, env []string) (*types.Project, error) {
return readProject(targetProjectFile, env)
}

func readProject(targetProjectFile string, env []string) (*types.Project, error) {
ctx := context.Background()
options, err := cli.NewProjectOptions(
[]string{targetProjectFile},
cli.WithResolvedPaths(false),
cli.WithNormalization(false),
cli.WithEnvFiles(),
cli.WithEnv(env),
)
if err != nil {
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion internal/deploy/docker/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func Deploy(ctx context.Context, output io.Writer, composeFile string, opts Depl
if err := term.PrintHeader(output, "Deployment Success"); err != nil {
return err
}
return post_deploy.PrintDeploySuccess(output, composeFile, post_deploy.DefaultMessage(composeFile))
return post_deploy.PrintDeploySuccess(output, composeFile, opts.TargetHost, post_deploy.DefaultMessage(composeFile))
}

func transferImagesViaPipe(ctx context.Context, output io.Writer, sourceHost, targetHost Host, composeFile string) error {
Expand Down
22 changes: 12 additions & 10 deletions internal/deploy/post_deploy/post_deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ package post_deploy
import (
"fmt"
"io"
"os"

cmdtext "github.com/arm/topo/internal/command"
"github.com/arm/topo/internal/compose"
"github.com/arm/topo/internal/project"
"github.com/arm/topo/internal/env"
"github.com/arm/topo/internal/ssh"
)

func DefaultMessage(composeFile string) string {
Expand All @@ -18,22 +18,24 @@ func DefaultMessage(composeFile string) string {
return fmt.Sprintf("Run `topo ps -f %s` to see deployed containers", cmdtext.QuoteArg(composeFile))
}

func getSuccessMessage(composeFile string) (string, error) {
f, err := os.Open(composeFile)
func getSuccessMessage(composeFile string, target ssh.Destination) (string, error) {
composeProject, err := compose.ReadProjectWithEnv(composeFile, env.ComposeEnv(target))
if err != nil {
return "", err
}
defer func() { _ = f.Close() }()

p, err := project.FromContent(f)
if err != nil {
var metadata struct {
DeploymentSuccessMessage string `mapstructure:"deployment_success_message"`
}
found, err := composeProject.Extensions.Get("x-topo", &metadata)
if err != nil || !found {
return "", err
}
return p.Metadata.DeploymentSuccessMessage, nil
return metadata.DeploymentSuccessMessage, nil
}

func PrintDeploySuccess(output io.Writer, composeFile, defaultMessage string) error {
successMessage, err := getSuccessMessage(composeFile)
func PrintDeploySuccess(output io.Writer, composeFile string, target ssh.Destination, defaultMessage string) error {
successMessage, err := getSuccessMessage(composeFile, target)
if err != nil {
return err
}
Expand Down
27 changes: 24 additions & 3 deletions internal/deploy/post_deploy/post_deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"

"github.com/arm/topo/internal/deploy/post_deploy"
"github.com/arm/topo/internal/ssh"
"github.com/arm/topo/internal/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand All @@ -24,7 +25,7 @@ services:
`)
var buf bytes.Buffer

err := post_deploy.PrintDeploySuccess(&buf, composeFile, "Run `topo ps` to see deployed containers")
err := post_deploy.PrintDeploySuccess(&buf, composeFile, ssh.PlainLocalhost, "Run `topo ps` to see deployed containers")

require.NoError(t, err)
assert.Equal(t, "Deployment complete!\n", buf.String())
Expand All @@ -40,16 +41,36 @@ services:
`)
var buf bytes.Buffer

err := post_deploy.PrintDeploySuccess(&buf, composeFile, "default message")
err := post_deploy.PrintDeploySuccess(&buf, composeFile, ssh.PlainLocalhost, "default message")

require.NoError(t, err)
assert.Equal(t, "default message\n", buf.String())
})

t.Run("interpolates the target in deployment_success_message", func(t *testing.T) {
dir := t.TempDir()
composeFile := filepath.Join(dir, "compose.yaml")
testutil.RequireWriteFile(t, composeFile, `
name: test-project
x-topo:
deployment_success_message: "${COMPOSE_PROJECT_NAME} deployed to ${TOPO_TARGET} at ${TOPO_TARGET_HOSTNAME}"
services:
app:
image: nginx
`)
var buf bytes.Buffer
target := ssh.NewDestination("user@localhost")

err := post_deploy.PrintDeploySuccess(&buf, composeFile, target, "default message")

require.NoError(t, err)
assert.Equal(t, "test-project deployed to ssh://user@localhost at localhost\n", buf.String())
})

t.Run("returns error when compose file does not exist", func(t *testing.T) {
var buf bytes.Buffer

err := post_deploy.PrintDeploySuccess(&buf, "nonexistent.yaml", "Run `topo ps` to see deployed containers")
err := post_deploy.PrintDeploySuccess(&buf, "nonexistent.yaml", ssh.PlainLocalhost, "Run `topo ps` to see deployed containers")

require.Error(t, err)
})
Expand Down
19 changes: 19 additions & 0 deletions internal/env/compose.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package env

import "github.com/arm/topo/internal/ssh"

const (
target = "TOPO_TARGET"
hostName = "TOPO_TARGET_HOSTNAME"
)

func variable(name, value string) string {
return name + "=" + value
}

func ComposeEnv(targetDestination ssh.Destination) []string {
return []string{
variable(target, targetDestination.String()),
variable(hostName, ssh.NewConfig(targetDestination).HostName),
}
}
21 changes: 21 additions & 0 deletions internal/env/compose_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package env_test

import (
"testing"

"github.com/arm/topo/internal/env"
"github.com/arm/topo/internal/ssh"
"github.com/stretchr/testify/assert"
)

func TestComposeEnv(t *testing.T) {
target := ssh.NewDestination("user@localhost")

got := env.ComposeEnv(target)

want := []string{
"TOPO_TARGET=ssh://user@localhost",
"TOPO_TARGET_HOSTNAME=localhost",
}
assert.Equal(t, want, got)
}
21 changes: 9 additions & 12 deletions internal/project/definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@ type Project struct {
}

type Metadata struct {
Name string
Description string
DeploymentSuccessMessage string
Features []string
Parameters []Parameter
Name string
Description string
Features []string
Parameters []Parameter
}

type Parameter struct {
Expand Down Expand Up @@ -47,12 +46,11 @@ func FromContent(reader io.Reader) (Project, error) {
}

type rawMetadata struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
DeploymentSuccessMessage string `yaml:"deployment_success_message"`
Features []string `yaml:"features,omitempty"`
Parameters map[string]rawParameter `yaml:"parameters,omitempty"`
Args map[string]rawParameter `yaml:"args,omitempty"`
Name string `yaml:"name"`
Description string `yaml:"description"`
Features []string `yaml:"features,omitempty"`
Parameters map[string]rawParameter `yaml:"parameters,omitempty"`
Args map[string]rawParameter `yaml:"args,omitempty"`
}

type rawParameter struct {
Expand All @@ -70,7 +68,6 @@ func (t *Metadata) UnmarshalYAML(node *yaml.Node) error {

t.Name = raw.Name
t.Description = raw.Description
t.DeploymentSuccessMessage = raw.DeploymentSuccessMessage
t.Features = raw.Features
parametersNode := findMetadataNode(node, "parameters")
parameters := raw.Parameters
Expand Down
29 changes: 0 additions & 29 deletions internal/project/definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,33 +144,4 @@ func TestFromContent(t *testing.T) {
}
assert.Equal(t, want, got)
})

t.Run("parses deployment_success_message from x-topo metadata", func(t *testing.T) {
composeFileContents := `
x-topo:
name: "test-service"
deployment_success_message: "Deployment complete!"
`
p, err := project.FromContent(strings.NewReader(composeFileContents))
got := p.Metadata

require.NoError(t, err)
want := project.Metadata{
Name: "test-service",
DeploymentSuccessMessage: "Deployment complete!",
}
assert.Equal(t, want, got)
})

t.Run("leaves DeploymentSuccessMessage empty when deployment_success_message absent from x-topo", func(t *testing.T) {
composeFileContents := `
x-topo:
name: "test-service"
`
p, err := project.FromContent(strings.NewReader(composeFileContents))
got := p.Metadata

require.NoError(t, err)
assert.Empty(t, got.DeploymentSuccessMessage)
})
}
Loading