Skip to content

Commit a5e02d8

Browse files
authored
Add definition to restrict deployment modes per input (#915)
In various Kibana issues, we found that agentless integrations needed specific inputs to be allowed/blocked. This is currently in a blocklist maintained in Fleet, but as more integrations become available for agentless, it is unsustainable for Fleet to maintain this list. Therefore the package spec needs to allow integrations to control what inputs are allowed on agentless, and Fleet should read from this information.
1 parent f44c0ab commit a5e02d8

File tree

10 files changed

+435
-0
lines changed

10 files changed

+435
-0
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package semantic
6+
7+
import (
8+
"fmt"
9+
"io/fs"
10+
11+
"gopkg.in/yaml.v3"
12+
13+
"github.com/elastic/package-spec/v3/code/go/internal/fspath"
14+
"github.com/elastic/package-spec/v3/code/go/pkg/specerrors"
15+
)
16+
17+
// ValidateDeploymentModes ensures that for each deployment mode enabled in a policy template,
18+
// there is at least one input that supports that deployment mode.
19+
func ValidateDeploymentModes(fsys fspath.FS) specerrors.ValidationErrors {
20+
manifestPath := "manifest.yml"
21+
d, err := fs.ReadFile(fsys, manifestPath)
22+
if err != nil {
23+
return specerrors.ValidationErrors{specerrors.NewStructuredErrorf("file \"%s\" is invalid: failed to read manifest: %w", fsys.Path(manifestPath), err)}
24+
}
25+
26+
var manifest struct {
27+
PolicyTemplates []struct {
28+
Name string `yaml:"name"`
29+
DeploymentModes struct {
30+
Default struct {
31+
Enabled *bool `yaml:"enabled"` // Use pointer to detect if field was set, default is true
32+
} `yaml:"default"`
33+
Agentless struct {
34+
Enabled bool `yaml:"enabled"`
35+
} `yaml:"agentless"`
36+
} `yaml:"deployment_modes"`
37+
Inputs []struct {
38+
Type string `yaml:"type"`
39+
DeploymentModes *[]string `yaml:"deployment_modes"` // Use pointer to detect if field was set
40+
} `yaml:"inputs"`
41+
} `yaml:"policy_templates"`
42+
}
43+
44+
err = yaml.Unmarshal(d, &manifest)
45+
if err != nil {
46+
return specerrors.ValidationErrors{specerrors.NewStructuredErrorf("file \"%s\" is invalid: failed to parse manifest: %w", fsys.Path(manifestPath), err)}
47+
}
48+
49+
var errs specerrors.ValidationErrors
50+
for _, template := range manifest.PolicyTemplates {
51+
// Collect enabled deployment modes for this policy template
52+
enabledModes := []string{}
53+
// Default mode is enabled by default, unless explicitly disabled
54+
if template.DeploymentModes.Default.Enabled == nil || *template.DeploymentModes.Default.Enabled {
55+
enabledModes = append(enabledModes, "default")
56+
}
57+
if template.DeploymentModes.Agentless.Enabled {
58+
enabledModes = append(enabledModes, "agentless")
59+
}
60+
61+
// Check each enabled deployment mode has at least one supporting input
62+
for _, enabledMode := range enabledModes {
63+
hasSupport := false
64+
for _, input := range template.Inputs {
65+
// If deployment_modes field was not specified, input supports all modes
66+
if input.DeploymentModes == nil {
67+
hasSupport = true
68+
break
69+
}
70+
// Check if this input explicitly supports the deployment mode
71+
for _, inputMode := range *input.DeploymentModes {
72+
if inputMode == enabledMode {
73+
hasSupport = true
74+
break
75+
}
76+
}
77+
if hasSupport {
78+
break
79+
}
80+
}
81+
82+
if !hasSupport {
83+
err := fmt.Errorf("file \"%s\" is invalid: policy template \"%s\" enables deployment mode \"%s\" but no input supports this mode", fsys.Path(manifestPath), template.Name, enabledMode)
84+
errs = append(errs, specerrors.NewStructuredError(err, specerrors.UnassignedCode))
85+
}
86+
}
87+
88+
// Check that input deployment modes are supported by the policy template
89+
for _, input := range template.Inputs {
90+
// If deployment_modes field was not specified, input supports all modes
91+
if input.DeploymentModes == nil {
92+
continue
93+
}
94+
// Check if the input has any deployment modes that are not enabled by the policy template
95+
for _, mode := range *input.DeploymentModes {
96+
found := false
97+
for _, enabledMode := range enabledModes {
98+
if mode == enabledMode {
99+
found = true
100+
break
101+
}
102+
}
103+
if !found {
104+
err := fmt.Errorf("file \"%s\" is invalid: input \"%s\" in policy template \"%s\" specifies unsupported deployment mode \"%s\"", fsys.Path(manifestPath), input.Type, template.Name, mode)
105+
errs = append(errs, specerrors.NewStructuredError(err, specerrors.UnassignedCode))
106+
}
107+
}
108+
}
109+
}
110+
111+
return errs
112+
}
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package semantic
6+
7+
import (
8+
"os"
9+
"path/filepath"
10+
"testing"
11+
12+
"github.com/elastic/package-spec/v3/code/go/internal/fspath"
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func TestValidateDeploymentModes(t *testing.T) {
18+
cases := []struct {
19+
title string
20+
manifestYAML string
21+
expectedErrs []string
22+
}{
23+
{
24+
title: "valid - inputs support all enabled deployment modes",
25+
manifestYAML: `
26+
policy_templates:
27+
- name: test
28+
deployment_modes:
29+
default:
30+
enabled: true
31+
agentless:
32+
enabled: true
33+
organization: elastic
34+
division: observability
35+
team: test
36+
inputs:
37+
- type: httpjson
38+
deployment_modes: ['default', 'agentless']
39+
- type: filestream
40+
deployment_modes: ['default']
41+
`,
42+
expectedErrs: nil,
43+
},
44+
{
45+
title: "valid - input with no deployment_modes supports all",
46+
manifestYAML: `
47+
policy_templates:
48+
- name: test
49+
deployment_modes:
50+
default:
51+
enabled: true
52+
agentless:
53+
enabled: true
54+
organization: elastic
55+
division: observability
56+
team: test
57+
inputs:
58+
- type: httpjson
59+
# No deployment_modes specified - supports all
60+
`,
61+
expectedErrs: nil,
62+
},
63+
{
64+
title: "invalid - default mode enabled but no input supports it",
65+
manifestYAML: `
66+
policy_templates:
67+
- name: test
68+
deployment_modes:
69+
agentless:
70+
enabled: true
71+
organization: elastic
72+
division: observability
73+
team: test
74+
inputs:
75+
- type: httpjson
76+
deployment_modes: ['agentless']
77+
`,
78+
expectedErrs: []string{
79+
`policy template "test" enables deployment mode "default" but no input supports this mode`,
80+
},
81+
},
82+
{
83+
title: "invalid - agentless mode enabled but no input supports it",
84+
manifestYAML: `
85+
policy_templates:
86+
- name: test
87+
deployment_modes:
88+
agentless:
89+
enabled: true
90+
organization: elastic
91+
division: observability
92+
team: test
93+
inputs:
94+
- type: httpjson
95+
deployment_modes: ['default']
96+
`,
97+
expectedErrs: []string{
98+
`policy template "test" enables deployment mode "agentless" but no input supports this mode`,
99+
},
100+
},
101+
{
102+
title: "invalid - both modes enabled but inputs support none",
103+
manifestYAML: `
104+
policy_templates:
105+
- name: test
106+
deployment_modes:
107+
agentless:
108+
enabled: true
109+
organization: elastic
110+
division: observability
111+
team: test
112+
inputs:
113+
- type: httpjson
114+
deployment_modes: []
115+
`,
116+
expectedErrs: []string{
117+
`policy template "test" enables deployment mode "default" but no input supports this mode`,
118+
`policy template "test" enables deployment mode "agentless" but no input supports this mode`,
119+
},
120+
},
121+
{
122+
title: "valid - no deployment modes enabled",
123+
manifestYAML: `
124+
policy_templates:
125+
- name: test
126+
inputs:
127+
- type: httpjson
128+
deployment_modes: ['default']
129+
`,
130+
expectedErrs: nil,
131+
},
132+
{
133+
title: "valid - multiple policy templates",
134+
manifestYAML: `
135+
policy_templates:
136+
- name: test1
137+
inputs:
138+
- type: httpjson
139+
deployment_modes: ['default']
140+
- name: test2
141+
deployment_modes:
142+
default:
143+
enabled: false
144+
agentless:
145+
enabled: true
146+
organization: elastic
147+
division: observability
148+
team: test
149+
inputs:
150+
- type: filestream
151+
deployment_modes: ['agentless']
152+
`,
153+
expectedErrs: nil,
154+
},
155+
{
156+
title: "invalid - input specifies unsupported deployment mode",
157+
manifestYAML: `
158+
policy_templates:
159+
- name: test
160+
deployment_modes:
161+
default:
162+
enabled: true
163+
inputs:
164+
- type: httpjson
165+
deployment_modes: ['agentless'] # agentless is disabled by default
166+
`,
167+
expectedErrs: []string{
168+
`policy template "test" enables deployment mode "default" but no input supports this mode`,
169+
`input "httpjson" in policy template "test" specifies unsupported deployment mode "agentless"`,
170+
},
171+
},
172+
{
173+
title: "invalid - input specifies multiple unsupported deployment modes",
174+
manifestYAML: `
175+
policy_templates:
176+
- name: test
177+
deployment_modes:
178+
default:
179+
enabled: false
180+
inputs:
181+
- type: httpjson
182+
deployment_modes: ['default', 'agentless'] # both disabled
183+
`,
184+
expectedErrs: []string{
185+
`input "httpjson" in policy template "test" specifies unsupported deployment mode "default"`,
186+
`input "httpjson" in policy template "test" specifies unsupported deployment mode "agentless"`,
187+
},
188+
},
189+
}
190+
191+
for _, c := range cases {
192+
t.Run(c.title, func(t *testing.T) {
193+
// Create a temporary directory and manifest file
194+
tempDir, err := os.MkdirTemp("", "test-deployment-modes")
195+
require.NoError(t, err)
196+
defer os.RemoveAll(tempDir)
197+
198+
manifestPath := filepath.Join(tempDir, "manifest.yml")
199+
err = os.WriteFile(manifestPath, []byte(c.manifestYAML), 0644)
200+
require.NoError(t, err)
201+
202+
fsys := fspath.DirFS(tempDir)
203+
errs := ValidateDeploymentModes(fsys)
204+
205+
if len(c.expectedErrs) == 0 {
206+
assert.Empty(t, errs)
207+
} else {
208+
require.Len(t, errs, len(c.expectedErrs))
209+
for i, expectedErr := range c.expectedErrs {
210+
assert.Contains(t, errs[i].Error(), expectedErr)
211+
}
212+
}
213+
})
214+
}
215+
}

code/go/internal/validator/spec.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ func (s Spec) rules(pkgType string, rootSpec spectypes.ItemSpec) validationRules
214214
{fn: semantic.ValidateDimensionsPresent, types: []string{"integration"}, since: semver.MustParse("3.0.1")},
215215
{fn: semantic.ValidateCapabilitiesRequired, since: semver.MustParse("2.10.0")}, // capabilities definition was added in spec version 2.10.0
216216
{fn: semantic.ValidateRequiredVarGroups},
217+
{fn: semantic.ValidateDeploymentModes, types: []string{"integration"}},
217218
}
218219

219220
var validationRules validationRules

code/go/pkg/validator/validator_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,17 @@ func TestValidateFile(t *testing.T) {
272272
`required var "password" in optional group is not defined`,
273273
},
274274
},
275+
"bad_input_deployment_modes": {
276+
"manifest.yml",
277+
[]string{
278+
`field policy_templates.0.inputs.0.deployment_modes.0: policy_templates.0.inputs.0.deployment_modes.0 must be one of the following: "default", "agentless"`,
279+
`field policy_templates.0.inputs.1.deployment_modes: Array must have at least 1 items`,
280+
`field policy_templates.0.inputs.2.deployment_modes: array items[0,1] must be unique`,
281+
`input "test/metrics" in policy template "test" specifies unsupported deployment mode "invalid_mode"`,
282+
`input "test/system" in policy template "test" specifies unsupported deployment mode "agentless"`,
283+
`policy template "unsupported_modes" enables deployment mode "default" but no input supports this mode`,
284+
},
285+
},
275286
"with_links": {},
276287
"bad_discovery_fields": {
277288
"manifest.yml",

spec/changelog.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
- description: Add support for `knowledge_base` directory under docs to store markdown files for AI assistant context.
2828
type: enhancement
2929
link: https://github.com/elastic/package-spec/pull/916
30+
- description: Add definition for input-level deployment modes.
31+
type: enhancement
32+
link: https://github.com/elastic/package-spec/pull/915
3033
- version: 3.4.0
3134
changes:
3235
- description: Add kibana/security_ai_prompt to support security AI prompt assets.

spec/integration/manifest.spec.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,22 @@ spec:
522522
description: Can input be defined multiple times
523523
type: boolean
524524
default: false
525+
deployment_modes:
526+
description: >
527+
List of deployment modes that this input is compatible with.
528+
If not specified, the input is compatible with all deployment modes.
529+
type: array
530+
minItems: 1
531+
uniqueItems: true
532+
items:
533+
type: string
534+
enum:
535+
- default
536+
- agentless
537+
examples:
538+
- ["default"]
539+
- ["agentless"]
540+
- ["default", "agentless"]
525541
required_vars:
526542
$ref: "./data_stream/manifest.spec.yml#/definitions/required_vars"
527543
vars:
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
- version: 0.0.1
2+
changes:
3+
- description: Initial version
4+
type: enhancement
5+
link: https://github.com/elastic/package-spec/pull/915
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Test Package
2+
3+
This is a test package for deployment modes validation.

0 commit comments

Comments
 (0)