Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

config: UI configuration block with Vault/Consul links #11555

Merged
merged 4 commits into from
Nov 24, 2021
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
12 changes: 12 additions & 0 deletions command/agent/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ type Config struct {
// parameters necessary to derive tokens.
Vault *config.VaultConfig `hcl:"vault"`

// UI is used to configure the web UI
UI *config.UIConfig `hcl:"ui"`

// NomadConfig is used to override the default config.
// This is largely used for testing purposes.
NomadConfig *nomad.Config `hcl:"-" json:"-"`
Expand Down Expand Up @@ -926,6 +929,7 @@ func DefaultConfig() *Config {
AdvertiseAddrs: &AdvertiseAddrs{},
Consul: config.DefaultConsulConfig(),
Vault: config.DefaultVaultConfig(),
UI: config.DefaultUIConfig(),
Client: &ClientConfig{
Enabled: false,
MaxKillTimeout: "30s",
Expand Down Expand Up @@ -1164,6 +1168,14 @@ func (c *Config) Merge(b *Config) *Config {
result.Vault = result.Vault.Merge(b.Vault)
}

// Apply the UI Configuration
if result.UI == nil && b.UI != nil {
uiConfig := *b.UI
result.UI = &uiConfig
} else if b.UI != nil {
result.UI = result.UI.Merge(b.UI)
}

// Apply the sentinel config
if result.Sentinel == nil && b.Sentinel != nil {
server := *b.Sentinel
Expand Down
15 changes: 12 additions & 3 deletions command/agent/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ var (
// Set to false by stub_asset if the ui build tag isn't enabled
uiEnabled = true

// Overridden if the ui build tag isn't enabled
stubHTML = ""
// Displayed when ui is disabled, but overridden if the ui build
// tag isn't enabled
stubHTML = "<html><p>Nomad UI is disabled</p></html>"

// allowCORS sets permissive CORS headers for a handler
allowCORS = cors.New(cors.Options{
Expand Down Expand Up @@ -336,13 +337,21 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) {
s.mux.HandleFunc("/v1/namespace", s.wrap(s.NamespaceCreateRequest))
s.mux.HandleFunc("/v1/namespace/", s.wrap(s.NamespaceSpecificRequest))

if uiEnabled {
uiConfigEnabled := s.agent.config.UI != nil && s.agent.config.UI.Enabled

if uiEnabled && uiConfigEnabled {
s.mux.Handle("/ui/", http.StripPrefix("/ui/", s.handleUI(http.FileServer(&UIAssetWrapper{FileSystem: assetFS()}))))
s.logger.Debug("UI is enabled")
} else {
// Write the stubHTML
s.mux.HandleFunc("/ui/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(stubHTML))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stubHTML is only set when building without the ui build tag. This results in the /ui endpoint to return a 200 and a empty page.

Looking at Vault and Consul, they each do different things. Vault returns 404 response code and a an error message:
image

Consul returns a 200 and generic content body:
image

Personally I would say Vault's seems to be the most correct approach, but no strong feelings. I think we could move this handler registration to the else clause below and add a generic 404 body content in the handleRootFallthrough handler.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'd intentionally not touched the existing behavior for the fallthrough, but a 404 does sound a lot nicer. Will do.

Copy link
Member Author

@tgross tgross Nov 24, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, having tried it just now I don't think that's very user friendly. If you go to http://localhost:4646 you end up getting a redirect to http://localhost:4646/ui with just the browser's 404 page. Having some stub content there and filling it out so that it does something more similar to Consul seems nicer.

(If the UI is disabled in the build, you get an explanation of that from https://github.com/hashicorp/nomad/blob/main/command/agent/stub_asset.go)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8006bd6

})
if uiEnabled && !uiConfigEnabled {
s.logger.Warn("UI is disabled")
} else {
s.logger.Debug("UI is disabled in this build")
}
}
s.mux.Handle("/", s.handleRootFallthrough())

Expand Down
130 changes: 130 additions & 0 deletions nomad/structs/config/ui.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package config

// UIConfig contains the operator configuration of the web UI
// Note:
// before extending this configuration, consider reviewing NMD-125
type UIConfig struct {

// Enabled is used to enable the web UI
Enabled bool `hcl:"enabled"`

// Consul configures deep links for Consul UI
Consul *ConsulUIConfig `hcl:"consul"`

// Vault configures deep links for Vault UI
Vault *VaultUIConfig `hcl:"vault"`
}

// ConsulUIConfig configures deep links to this cluster's Consul UI
type ConsulUIConfig struct {

// BaseURL provides the full base URL, ex:
// https://consul.example.com:8500/ui/
BaseURL string `hcl:"base_url"`
}

// VaultUIConfig configures deep links to this cluster's Vault UI
type VaultUIConfig struct {
// BaseURL provides the full base URL, ex:
// https://vault.example.com:8200/ui/
BaseURL string `hcl:"base_url"`
}

// DefaultUIConfig returns the canonical defaults for the Nomad
// `ui` configuration.
func DefaultUIConfig() *UIConfig {
return &UIConfig{
Enabled: true,
Consul: &ConsulUIConfig{},
Vault: &VaultUIConfig{},
}
}

// Copy returns a copy of this UI config.
func (old *UIConfig) Copy() *UIConfig {
if old == nil {
return nil
}

nc := new(UIConfig)
*nc = *old

if old.Consul != nil {
nc.Consul = old.Consul.Copy()
}
if old.Vault != nil {
nc.Vault = old.Vault.Copy()
}
return nc
}

// Merge returns a new UI configuration by merging another UI
// configuration into this one
func (this *UIConfig) Merge(other *UIConfig) *UIConfig {
result := this.Copy()
if other == nil {
return result
}

result.Enabled = other.Enabled
result.Consul = result.Consul.Merge(other.Consul)
result.Vault = result.Vault.Merge(other.Vault)

return result
}

// Copy returns a copy of this Consul UI config.
func (old *ConsulUIConfig) Copy() *ConsulUIConfig {
if old == nil {
return nil
}

nc := new(ConsulUIConfig)
*nc = *old
return nc
}

// Merge returns a new Consul UI configuration by merging another Consul UI
// configuration into this one
func (this *ConsulUIConfig) Merge(other *ConsulUIConfig) *ConsulUIConfig {
result := this.Copy()
if result == nil {
result = &ConsulUIConfig{}
}
if other == nil {
return result
}

if other.BaseURL != "" {
result.BaseURL = other.BaseURL
}
return result
}

// Copy returns a copy of this Vault UI config.
func (old *VaultUIConfig) Copy() *VaultUIConfig {
if old == nil {
return nil
}

nc := new(VaultUIConfig)
*nc = *old
return nc
}

// Merge returns a new Vault UI configuration by merging another Vault UI
// configuration into this one
func (this *VaultUIConfig) Merge(other *VaultUIConfig) *VaultUIConfig {
result := this.Copy()
if result == nil {
result = &VaultUIConfig{}
}
if other == nil {
return result
}

if other.BaseURL != "" {
result.BaseURL = other.BaseURL
}
return result
}
78 changes: 78 additions & 0 deletions nomad/structs/config/ui_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package config

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestUIConfig_Merge(t *testing.T) {

fullConfig := &UIConfig{
Enabled: true,
Consul: &ConsulUIConfig{
BaseURL: "http://consul.example.com:8500",
},
Vault: &VaultUIConfig{
BaseURL: "http://vault.example.com:8200",
},
}

testCases := []struct {
name string
left *UIConfig
right *UIConfig
expect *UIConfig
}{
{
name: "merge onto empty config",
left: &UIConfig{},
right: fullConfig,
expect: fullConfig,
},
{
name: "merge in a nil config",
left: fullConfig,
right: nil,
expect: fullConfig,
},
{
name: "merge onto zero-values",
left: &UIConfig{
Enabled: false,
Consul: &ConsulUIConfig{
BaseURL: "http://consul-other.example.com:8500",
},
},
right: fullConfig,
expect: fullConfig,
},
{
name: "merge from zero-values",
left: &UIConfig{
Enabled: true,
Consul: &ConsulUIConfig{
BaseURL: "http://consul-other.example.com:8500",
},
},
right: &UIConfig{},
expect: &UIConfig{
Enabled: false,
Consul: &ConsulUIConfig{
BaseURL: "http://consul-other.example.com:8500",
},
Vault: &VaultUIConfig{},
},
},
}

for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
result := tc.left.Merge(tc.right)
require.Equal(t, tc.expect, result)
})
}

}
69 changes: 69 additions & 0 deletions website/content/docs/configuration/ui.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
layout: docs
page_title: ui Stanza - Agent Configuration
description: |-
The "ui" stanza configures the Nomad agent's web UI.

---

# `ui` Stanza

<Placement groups={['ui']} />

The `ui` stanza configures the Nomad agent's [web UI].

```hcl
ui {
enabled = true

consul {
base_url = "https://consul.example.com:8500/ui"
}

vault {
base_url = "https://vault.example.com:8200/ui"
}
}
```

A default `ui` stanza is automatically merged with all Nomad agent
configurations. Note that the UI can be served from any Nomad agent,
and the configuration is individual to each agent.
## `ui` Parameters
- `enabled` `(bool: true)` - Specifies whether the web UI is
enabled. If disabled, the `/ui/` path will return an empty web page.

- `consul` <code>([Consul]: nil)</code> - Configures integrations
between the Nomad web UI and the Consul web UI.

- `vault` <code>([Vault]: nil)</code> - Configures integrations
between the Nomad web UI and the Vault web UI.

## `consul` Parameters

- `base_url` `(string: "")` - Specifies the full base URL to a Consul
web UI (for example: `https://consul.example.com:8500/ui`. This URL
is used to build links from the Nomad web UI to a Consul web
UI. Note that this URL will not typically be the same one used for
the agent's [`consul.address`]; the `consul.address` is the URL used
by the Nomad to communicate with Consul, whereas the
`ui.consul.base_url` is the URL you'll visit in your browser. If
this field is omitted, this integration will be disabled.

## `vault` Parameters

- `base_url` `(string: "")` - Specifies the full base URL to a Vault
web UI (for example: `https://vault.example.com:8200/ui`. This URL
is used to build links from the Nomad web UI to a Vault web
UI. Note that this URL will not typically be the same one used for
the agent's [`vault.address`]; the `vault.address` is the URL used
by the Nomad to communicate with Vault, whereas the
`ui.vault.base_url` is the URL you'll visit in your browser. If
this field is omitted, this integration will be disabled.


[web UI]: https://learn.hashicorp.com/collections/nomad/web-ui
[Consul]: /docs/configuration/ui#consul-parameters
[Vault]: /docs/configuration/ui#vault-parameters
[`consul.address`]: /docs/configuration/consul#address
[`vault.address`]: /docs/configuration/vault#address
4 changes: 4 additions & 0 deletions website/data/docs-nav-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@
"title": "tls",
"path": "configuration/tls"
},
{
"title": "ui",
"path": "configuration/ui"
},
{
"title": "vault",
"path": "configuration/vault"
Expand Down