Skip to content

Commit 7c335af

Browse files
Add support for release field in agentless deployment mode (#1130)
* Add support for field in agentless deployment mode * Add PR link to changelog * Remove 'rc' from agentless release enum * Extract validateAgentlessReleaseDeployment, add tests * Formatting * Update code/go/internal/validator/semantic/validate_deployment_modes.go Co-authored-by: Tere <romero.teresa@protonmail.com> * Formatting * Change agentless deployment mode 'release' default to 'beta' * Undo formatting changes * Clarify var name * Address PR feedback * Update description of deployment mode release field * Move changelog entry from next patch to next minor --------- Co-authored-by: Tere <romero.teresa@protonmail.com>
1 parent 995a94e commit 7c335af

9 files changed

Lines changed: 215 additions & 19 deletions

File tree

code/go/internal/validator/semantic/validate_deployment_modes.go

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,35 @@ import (
1414
"github.com/elastic/package-spec/v3/code/go/pkg/specerrors"
1515
)
1616

17+
type deploymentModesManifest struct {
18+
PolicyTemplates []deploymentModesPolicyTemplate `yaml:"policy_templates"`
19+
}
20+
21+
type deploymentModesPolicyTemplate struct {
22+
Name string `yaml:"name"`
23+
DeploymentModes deploymentModesSpec `yaml:"deployment_modes"`
24+
Inputs []deploymentModesInput `yaml:"inputs"`
25+
}
26+
27+
type deploymentModesSpec struct {
28+
Default deploymentModesDefault `yaml:"default"`
29+
Agentless deploymentModesAgentless `yaml:"agentless"`
30+
}
31+
32+
type deploymentModesDefault struct {
33+
Enabled *bool `yaml:"enabled"` // pointer to detect if field was set; when unset (nil) semantical meaning is default deployment is enabled
34+
}
35+
36+
type deploymentModesAgentless struct {
37+
Enabled bool `yaml:"enabled"`
38+
Release string `yaml:"release"`
39+
}
40+
41+
type deploymentModesInput struct {
42+
Type string `yaml:"type"`
43+
DeploymentModes *[]string `yaml:"deployment_modes"` // pointer to detect if field was set
44+
}
45+
1746
// ValidateDeploymentModes ensures that for each deployment mode enabled in a policy template,
1847
// there is at least one input that supports that deployment mode.
1948
func ValidateDeploymentModes(fsys fspath.FS) specerrors.ValidationErrors {
@@ -23,30 +52,18 @@ func ValidateDeploymentModes(fsys fspath.FS) specerrors.ValidationErrors {
2352
return specerrors.ValidationErrors{specerrors.NewStructuredErrorf("file \"%s\" is invalid: failed to read manifest: %w", fsys.Path(manifestPath), err)}
2453
}
2554

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-
55+
var manifest deploymentModesManifest
4456
err = yaml.Unmarshal(d, &manifest)
4557
if err != nil {
4658
return specerrors.ValidationErrors{specerrors.NewStructuredErrorf("file \"%s\" is invalid: failed to parse manifest: %w", fsys.Path(manifestPath), err)}
4759
}
4860

4961
var errs specerrors.ValidationErrors
62+
63+
if err := validateAgentlessReleaseDeployment(manifest); err != nil {
64+
errs = append(errs, specerrors.NewStructuredError(fmt.Errorf("file \"%s\" is invalid: %w", fsys.Path(manifestPath), err), specerrors.UnassignedCode))
65+
}
66+
5067
for _, template := range manifest.PolicyTemplates {
5168
// Collect enabled deployment modes for this policy template
5269
enabledModes := []string{}
@@ -110,3 +127,20 @@ func ValidateDeploymentModes(fsys fspath.FS) specerrors.ValidationErrors {
110127

111128
return errs
112129
}
130+
131+
// validateAgentlessReleaseDeployment checks that agentless.release is not set in a
132+
// single-policy-template package where agentless is the only deployment mode. In that
133+
// case the package version is the authoritative source of maturity and an explicit
134+
// override would conflict.
135+
func validateAgentlessReleaseDeployment(manifest deploymentModesManifest) error {
136+
if len(manifest.PolicyTemplates) != 1 {
137+
return nil
138+
}
139+
tmpl := manifest.PolicyTemplates[0]
140+
// Default.Enabled == nil means default mode is implicitly enabled, so agentless is not the only mode.
141+
defaultDeploymentDisabled := tmpl.DeploymentModes.Default.Enabled != nil && !*tmpl.DeploymentModes.Default.Enabled
142+
if defaultDeploymentDisabled && tmpl.DeploymentModes.Agentless.Release != "" {
143+
return fmt.Errorf("policy template \"%s\" sets agentless.release but agentless is the only deployment mode; use the package version to indicate maturity instead", tmpl.Name)
144+
}
145+
return nil
146+
}

code/go/internal/validator/semantic/validate_deployment_modes_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,104 @@ policy_templates:
214214
})
215215
}
216216
}
217+
218+
func boolPtr(b bool) *bool { return &b }
219+
220+
func TestValidateAgentlessReleaseDeployment(t *testing.T) {
221+
cases := []struct {
222+
title string
223+
manifest deploymentModesManifest
224+
expectedErr string
225+
}{
226+
{
227+
title: "valid - default enabled explicitly, release set",
228+
manifest: deploymentModesManifest{
229+
PolicyTemplates: []deploymentModesPolicyTemplate{
230+
{
231+
Name: "test",
232+
DeploymentModes: deploymentModesSpec{
233+
Default: deploymentModesDefault{Enabled: boolPtr(true)},
234+
Agentless: deploymentModesAgentless{Enabled: true, Release: "ga"},
235+
},
236+
},
237+
},
238+
},
239+
},
240+
{
241+
title: "valid - default enabled implicitly (nil), release set",
242+
manifest: deploymentModesManifest{
243+
PolicyTemplates: []deploymentModesPolicyTemplate{
244+
{
245+
Name: "test",
246+
DeploymentModes: deploymentModesSpec{
247+
Default: deploymentModesDefault{Enabled: nil},
248+
Agentless: deploymentModesAgentless{Enabled: true, Release: "ga"},
249+
},
250+
},
251+
},
252+
},
253+
},
254+
{
255+
title: "valid - agentless-only single template, no release set",
256+
manifest: deploymentModesManifest{
257+
PolicyTemplates: []deploymentModesPolicyTemplate{
258+
{
259+
Name: "test",
260+
DeploymentModes: deploymentModesSpec{
261+
Default: deploymentModesDefault{Enabled: boolPtr(false)},
262+
Agentless: deploymentModesAgentless{Enabled: true},
263+
},
264+
},
265+
},
266+
},
267+
},
268+
{
269+
title: "valid - multiple templates, one agentless-only with release set",
270+
manifest: deploymentModesManifest{
271+
PolicyTemplates: []deploymentModesPolicyTemplate{
272+
{
273+
Name: "test1",
274+
DeploymentModes: deploymentModesSpec{
275+
Default: deploymentModesDefault{Enabled: boolPtr(true)},
276+
Agentless: deploymentModesAgentless{Enabled: true},
277+
},
278+
},
279+
{
280+
Name: "test2",
281+
DeploymentModes: deploymentModesSpec{
282+
Default: deploymentModesDefault{Enabled: boolPtr(false)},
283+
Agentless: deploymentModesAgentless{Enabled: true, Release: "ga"},
284+
},
285+
},
286+
},
287+
},
288+
},
289+
{
290+
title: "invalid - agentless-only single template with release set",
291+
manifest: deploymentModesManifest{
292+
PolicyTemplates: []deploymentModesPolicyTemplate{
293+
{
294+
Name: "test",
295+
DeploymentModes: deploymentModesSpec{
296+
Default: deploymentModesDefault{Enabled: boolPtr(false)},
297+
Agentless: deploymentModesAgentless{Enabled: true, Release: "ga"},
298+
},
299+
},
300+
},
301+
},
302+
expectedErr: `policy template "test" sets agentless.release but agentless is the only deployment mode; use the package version to indicate maturity instead`,
303+
},
304+
}
305+
306+
for _, c := range cases {
307+
t.Run(c.title, func(t *testing.T) {
308+
err := validateAgentlessReleaseDeployment(c.manifest)
309+
if c.expectedErr == "" {
310+
assert.NoError(t, err)
311+
} else {
312+
require.Error(t, err)
313+
assert.Contains(t, err.Error(), c.expectedErr)
314+
}
315+
})
316+
}
317+
}

code/go/pkg/validator/validator_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,12 @@ func TestValidateFile(t *testing.T) {
379379
`var "access_key_id" in non-required var_group "credential_type" should not have required: true (var_group is optional)`,
380380
},
381381
},
382+
"bad_agentless_release": {
383+
"manifest.yml",
384+
[]string{
385+
`policy template "test" sets agentless.release but agentless is the only deployment mode; use the package version to indicate maturity instead`,
386+
},
387+
},
382388
"bad_input_deployment_modes": {
383389
"manifest.yml",
384390
[]string{

spec/changelog.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
- description: Add support for semantic_text field definition.
99
type: enhancement
1010
link: https://github.com/elastic/package-spec/pull/807
11+
- description: Add optional `release` field to agentless deployment mode to explicitly declare its release stage.
12+
type: enhancement
13+
link: https://github.com/elastic/package-spec/pull/1130
1114
- version: 3.6.2
1215
changes:
1316
- description: Add support for ML modules in content packages.

spec/integration/manifest.spec.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,19 @@ spec:
295295
type: string
296296
examples:
297297
- "cloud-security-posture-management"
298+
release:
299+
description: >
300+
The maturity level of the agentless deployment mode for this policy template.
301+
If not defined, Kibana will provide a default value based on agentless platform maturity.
302+
Packages where agentless is the only deployment mode, should defer to the package's top-level `version`.
303+
Only evaluated in Kibana 9.5.0 or later.
304+
type: string
305+
enum:
306+
- beta
307+
- ga
308+
examples:
309+
- beta
310+
- ga
298311
resources:
299312
description: >
300313
The computing resources specifications for the Agentless deployment.
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/1130
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 agentless release validation.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
format_version: 3.6.1
2+
name: bad_agentless_release
3+
title: Bad Agentless Release
4+
description: Test package where agentless.release is set but agentless is the only deployment mode
5+
version: 0.0.1
6+
type: integration
7+
policy_templates:
8+
- name: test
9+
title: Test
10+
description: Test policy template
11+
deployment_modes:
12+
default:
13+
enabled: false
14+
agentless:
15+
enabled: true
16+
release: ga
17+
organization: elastic
18+
division: observability
19+
team: test
20+
inputs:
21+
- type: httpjson
22+
title: Test HTTP JSON
23+
description: Test HTTP JSON input
24+
deployment_modes: ['agentless']
25+
conditions:
26+
kibana:
27+
version: '^9.5.0'
28+
owner:
29+
github: elastic/ecosystem
30+
type: elastic

test/packages/good_v3/manifest.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ source:
88
license: "Apache-2.0"
99
conditions:
1010
kibana:
11-
version: '^8.10.0'
11+
version: '^9.5.0'
1212
elastic:
1313
subscription: 'basic'
1414
capabilities:
@@ -39,6 +39,7 @@ policy_templates:
3939
agentless:
4040
enabled: true
4141
is_default: true
42+
release: ga
4243
organization: elastic
4344
division: observability
4445
team: obs-infraobs-integrations

0 commit comments

Comments
 (0)