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
35 changes: 32 additions & 3 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package config

import (
"fmt"
"os"

"github.com/goccy/go-yaml"
"github.com/goccy/go-yaml/ast"
"os"
"github.com/netapp/ontap-mcp/third_party/mergo"
)

const (
Expand All @@ -31,14 +33,41 @@ func ReadConfig(path string) (*ONTAP, error) {
}
cfg.PollersOrdered = orderedConfig.Pollers.namesInOrder

// Set the Name field for each poller
// Set the Name field for each poller and apply Defaults for any
// key that is missing from the poller's own configuration.
for name, poller := range cfg.Pollers {
poller.Name = name
if err := poller.applyDefaults(cfg.Defaults); err != nil {
return nil, fmt.Errorf("error applying defaults to poller %q: %w", name, err)
}
Comment thread
cgrinds marked this conversation as resolved.
}

return &cfg, nil
}

// applyDefaults fills any unset (zero-valued) field on the poller with the
// corresponding value from defaults. A value explicitly set on the poller
// always takes precedence over the default. Nested structs are merged
// field-by-field, so a poller can override one sub-field while inheriting
// the rest.
//
// Pointer fields such as UseInsecureTLS distinguish "unset" (nil) from an
// explicit value. mergo.WithoutDereference keeps a non-nil pointer on the
// poller intact, so a poller can override a true default back to false.
func (p *Poller) applyDefaults(defaults *Poller) error {
if defaults == nil {
return nil
}

return mergo.Merge(p, *defaults, mergo.WithoutDereference)
}

// InsecureTLS reports the effective use_insecure_tls value, treating an unset
// (nil) field as false.
func (p *Poller) InsecureTLS() bool {
return p.UseInsecureTLS != nil && *p.UseInsecureTLS
}

type ONTAP struct {
Pollers map[string]*Poller `yaml:"Pollers,omitempty"`
Defaults *Poller `yaml:"Defaults,omitempty"`
Expand All @@ -59,7 +88,7 @@ type Poller struct {
Recorder Recorder `yaml:"recorder,omitempty"`
SslCert string `yaml:"ssl_cert,omitempty"`
SslKey string `yaml:"ssl_key,omitempty"`
UseInsecureTLS bool `yaml:"use_insecure_tls,omitempty"`
UseInsecureTLS *bool `yaml:"use_insecure_tls,omitempty"`
Username string `yaml:"username,omitempty"`
Name string
}
Expand Down
191 changes: 191 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package config

import (
"os"
"path/filepath"
"testing"

"github.com/netapp/ontap-mcp/assert"
)

func TestApplyDefaults_FillsMissingKeys(t *testing.T) {
defaults := &Poller{
Username: "admin",
Password: "password",
UseInsecureTLS: new(true),
}
poller := &Poller{
Addr: "10.0.0.1",
}

assert.Nil(t, poller.applyDefaults(defaults))

assert.Equal(t, poller.Username, "admin")
assert.Equal(t, poller.Password, "password")
assert.True(t, poller.InsecureTLS())
assert.Equal(t, poller.Addr, "10.0.0.1")
}

func TestApplyDefaults_PollerOverridesDefault(t *testing.T) {
defaults := &Poller{
Username: "admin",
Password: "password",
}
poller := &Poller{
Username: "operator",
Password: "secret",
}

assert.Nil(t, poller.applyDefaults(defaults))

assert.Equal(t, poller.Username, "operator")
assert.Equal(t, poller.Password, "secret")
}

func TestApplyDefaults_MergesNestedCredentialsScript(t *testing.T) {
defaults := &Poller{
CredentialsScript: CredentialsScript{
Path: "/default/get_pass",
Schedule: "always",
Timeout: "5s",
},
}
poller := &Poller{
CredentialsScript: CredentialsScript{
Path: "/poller/get_pass",
},
}

assert.Nil(t, poller.applyDefaults(defaults))

assert.Equal(t, poller.CredentialsScript.Path, "/poller/get_pass")
assert.Equal(t, poller.CredentialsScript.Schedule, "always")
assert.Equal(t, poller.CredentialsScript.Timeout, "5s")
}

func TestApplyDefaults_NilDefaultsIsNoOp(t *testing.T) {
poller := &Poller{
Addr: "10.0.0.1",
Username: "operator",
}

assert.Nil(t, poller.applyDefaults(nil))

assert.Equal(t, poller.Addr, "10.0.0.1")
assert.Equal(t, poller.Username, "operator")
assert.Equal(t, poller.Password, "")
}

func TestReadConfig_AppliesDefaultsAcrossPollers(t *testing.T) {
yamlContent := `
Defaults:
username: admin
password: password
use_insecure_tls: true

Pollers:
inherits-all:
addr: 10.0.0.1
overrides-user:
addr: 10.0.0.2
username: operator
password: secret
`
dir := t.TempDir()
path := filepath.Join(dir, "ontap.yaml")
if err := os.WriteFile(path, []byte(yamlContent), 0o600); err != nil {
t.Fatalf("write temp config: %v", err)
}

cfg, err := ReadConfig(path)
assert.Nil(t, err)

inherits := cfg.Pollers["inherits-all"]
assert.NotNil(t, inherits)
assert.Equal(t, inherits.Username, "admin")
assert.Equal(t, inherits.Password, "password")
assert.True(t, inherits.InsecureTLS())
assert.Equal(t, inherits.Name, "inherits-all")

overrides := cfg.Pollers["overrides-user"]
assert.NotNil(t, overrides)
assert.Equal(t, overrides.Username, "operator")
assert.Equal(t, overrides.Password, "secret")
assert.True(t, overrides.InsecureTLS())
}

func TestApplyDefaults_PollerOverridesTrueDefaultWithFalse(t *testing.T) {
defaults := &Poller{
UseInsecureTLS: new(true),
}
poller := &Poller{
UseInsecureTLS: new(false),
}

assert.Nil(t, poller.applyDefaults(defaults))

assert.NotNil(t, poller.UseInsecureTLS)
assert.False(t, poller.InsecureTLS())
}

func TestApplyDefaults_PollerInheritsUnsetInsecureTLS(t *testing.T) {
defaults := &Poller{
UseInsecureTLS: new(true),
}
poller := &Poller{}

assert.Nil(t, poller.applyDefaults(defaults))

assert.NotNil(t, poller.UseInsecureTLS)
assert.True(t, poller.InsecureTLS())
}

func TestApplyDefaults_PollerKeepsTrueOverFalseDefault(t *testing.T) {
defaults := &Poller{
UseInsecureTLS: new(false),
}
poller := &Poller{
UseInsecureTLS: new(true),
}

assert.Nil(t, poller.applyDefaults(defaults))

assert.True(t, poller.InsecureTLS())
}

func TestReadConfig_PollerOverridesInsecureTLSToFalse(t *testing.T) {
yamlContent := `
Defaults:
username: admin
use_insecure_tls: true

Pollers:
inherits-tls:
addr: 10.0.0.1
secure-poller:
addr: 10.0.0.2
use_insecure_tls: false
`
dir := t.TempDir()
path := filepath.Join(dir, "ontap.yaml")
if err := os.WriteFile(path, []byte(yamlContent), 0o600); err != nil {
t.Fatalf("write temp config: %v", err)
}

cfg, err := ReadConfig(path)
assert.Nil(t, err)

inherits := cfg.Pollers["inherits-tls"]
assert.NotNil(t, inherits)
assert.True(t, inherits.InsecureTLS())

secure := cfg.Pollers["secure-poller"]
assert.NotNil(t, secure)
assert.False(t, secure.InsecureTLS())
}

func TestInsecureTLS_NilFieldIsFalse(t *testing.T) {
poller := &Poller{}

assert.False(t, poller.InsecureTLS())
}
18 changes: 9 additions & 9 deletions docs/prepare-ontap.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ Pollers:

Below is a table describing the configuration options:

| Option | Type | Description | Default |
|----------------------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
| poller-name | required | The IP address or hostname of the ONTAP cluster. | - |
| `addr` | required | The IPv4, IPv6 or FQDN of the ONTAP cluster. | - |
| `username` | | The username for authentication. | - |
| `password` | | The password for authentication. Not recommended for production use. Use `credentials_script` or `credentials_file` instead. See [authentication](#authentication) for details | - |
| `use_insecure_tls` | optional, bool | Set to `true` to allow insecure TLS connections (e.g., self-signed certificates). Not recommended for production use. | false |
| `credentials_file` | optional, string | Path to a yaml file that contains cluster credentials. The file should have the same shape as ontap.yaml. Path can be relative to ontap.yaml or absolute. | |
| `credentials_script` | optional, section | Section that defines how ONTAP-MCP should fetch credentials via external script. See [here](#credentials-script) for details. | |
| Option | Type | Description | Default |
|----------------------|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
| poller-name | required | The IP address or hostname of the ONTAP cluster. | - |
| `addr` | required | The IPv4, IPv6 or FQDN of the ONTAP cluster. | - |
| `username` | | The username for authentication. | - |
| `password` | | The password for authentication. Not recommended for production use. Use `credentials_script` or `credentials_file` instead. See [authentication](#authentication) for details | - |
| `use_insecure_tls` | optional, bool | Set to `true` to allow insecure TLS connections (e.g., self-signed certificates). Not recommended for production use. When set in the `Defaults` section, an individual poller may override it back to `false`. | false |
| `credentials_file` | optional, string | Path to a yaml file that contains cluster credentials. The file should have the same shape as ontap.yaml. Path can be relative to ontap.yaml or absolute. | |
| `credentials_script` | optional, section | Section that defines how ONTAP-MCP should fetch credentials via external script. See [here](#credentials-script) for details. | |

# Authentication

Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ require (
github.com/segmentio/asm v1.2.1 // indirect
github.com/segmentio/encoding v0.5.4 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.44.0 // indirect
golang.org/x/sys v0.46.0 // indirect
)
20 changes: 8 additions & 12 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+
github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/modelcontextprotocol/go-sdk v1.6.0-pre.1 h1:wAz+jUrWmkDOnD1fSba+inmp7dzMHg7yG3jv7XU3+mc=
github.com/modelcontextprotocol/go-sdk v1.6.0-pre.1/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ=
github.com/modelcontextprotocol/go-sdk v1.6.0 h1:PPLS3kn7WtOEnR+Af4X5H96SG0qSab8R/ZQT/HkhPkY=
github.com/modelcontextprotocol/go-sdk v1.6.0/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ=
github.com/modelcontextprotocol/go-sdk v1.6.1 h1:0zOSupjKUxPKSocPT1Wtago+mUHU2/uZ4xSOY0FGReU=
github.com/modelcontextprotocol/go-sdk v1.6.1/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
Expand All @@ -28,15 +24,15 @@ github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfv
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
16 changes: 9 additions & 7 deletions integration/go.mod
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
module github.com/Netapp/ontap-mcp-automation

go 1.26.1
go 1.26.3

replace github.com/netapp/ontap-mcp => ../

require (
github.com/carlmjohnson/requests v0.25.1
github.com/modelcontextprotocol/go-sdk v1.4.0
github.com/netapp/ontap-mcp v0.0.0-20260310161402-013e518b9fc4
github.com/openai/openai-go/v3 v3.26.0
github.com/modelcontextprotocol/go-sdk v1.6.1
github.com/netapp/ontap-mcp v0.0.0-20260615122109-078d56edbf94
github.com/openai/openai-go/v3 v3.39.0
)

require (
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/google/jsonschema-go v0.4.2 // indirect
github.com/google/jsonschema-go v0.4.3 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/segmentio/encoding v0.5.4 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/sys v0.46.0 // indirect
)
Loading
Loading