Skip to content

Commit 39bb63f

Browse files
committed
feat(plan): add plan section support
1 parent 6bbc91a commit 39bb63f

File tree

6 files changed

+933
-57
lines changed

6 files changed

+933
-57
lines changed

internals/plan/export_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) 2024 Canonical Ltd
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU General Public License version 3 as
5+
// published by the Free Software Foundation.
6+
//
7+
// This program is distributed in the hope that it will be useful,
8+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
9+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10+
// GNU General Public License for more details.
11+
//
12+
// You should have received a copy of the GNU General Public License
13+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
14+
15+
package plan
16+
17+
// ResetLayerExtensions resets the global state between tests.
18+
func ResetLayerExtensions() {
19+
layerExtensions = map[string]LayerSectionExtension{}
20+
}

internals/plan/package_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) 2024 Canonical Ltd
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU General Public License version 3 as
5+
// published by the Free Software Foundation.
6+
//
7+
// This program is distributed in the hope that it will be useful,
8+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
9+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10+
// GNU General Public License for more details.
11+
//
12+
// You should have received a copy of the GNU General Public License
13+
// along with this progral. If not, see <http://www.gnu.org/licenses/>.
14+
15+
package plan_test
16+
17+
import (
18+
"testing"
19+
20+
. "gopkg.in/check.v1"
21+
22+
"github.com/canonical/pebble/internals/plan"
23+
)
24+
25+
// Hook up check.v1 into the "go test" runner.
26+
func Test(t *testing.T) { TestingT(t) }
27+
28+
type planSuite struct{}
29+
30+
var _ = Suite(&planSuite{})
31+
32+
func (ps *planSuite) SetUpTest(c *C) {
33+
plan.ResetLayerExtensions()
34+
}

internals/plan/plan.go

Lines changed: 176 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,32 @@ import (
3232
"github.com/canonical/pebble/internals/osutil"
3333
)
3434

35+
// LayerSectionExtension allows the plan layer schema to be extended without
36+
// adding centralised schema knowledge to the plan library.
37+
type LayerSectionExtension interface {
38+
// ParseSection creates a new layer section containing the unmarshalled
39+
// yaml.Node, and any additional section specifics it wishes to apply
40+
// to the backing type. A nil LayerSection returned ensures that this
41+
// section will be omitted by the caller.
42+
ParseSection(data *yaml.Node) (LayerSection, error)
43+
44+
// CombineSections creates a new layer section containing the result of
45+
// combining the layer sections in the supplied order. A nil LayerSection
46+
// returned ensures the combined section will be completely omitted by
47+
// the caller.
48+
CombineSections(sections ...LayerSection) (LayerSection, error)
49+
50+
// ValidatePlan takes the complete plan as input, and allows the
51+
// extension to validate the plan. This can be used for cross section
52+
// dependency validation.
53+
ValidatePlan(plan *Plan) error
54+
}
55+
56+
type LayerSection interface {
57+
// Validate expects the section to validate itself.
58+
Validate() error
59+
}
60+
3561
const (
3662
defaultBackoffDelay = 500 * time.Millisecond
3763
defaultBackoffFactor = 2.0
@@ -42,11 +68,33 @@ const (
4268
defaultCheckThreshold = 3
4369
)
4470

71+
// layerExtensions keeps a map of registered extensions.
72+
var layerExtensions = map[string]LayerSectionExtension{}
73+
74+
// RegisterExtension must be called by the plan extension owners to
75+
// extend the plan schema before the plan is loaded.
76+
func RegisterExtension(field string, ext LayerSectionExtension) error {
77+
if _, ok := layerExtensions[field]; ok {
78+
return fmt.Errorf("internal error: extension %q already registered", field)
79+
}
80+
layerExtensions[field] = ext
81+
return nil
82+
}
83+
4584
type Plan struct {
4685
Layers []*Layer `yaml:"-"`
4786
Services map[string]*Service `yaml:"services,omitempty"`
4887
Checks map[string]*Check `yaml:"checks,omitempty"`
4988
LogTargets map[string]*LogTarget `yaml:"log-targets,omitempty"`
89+
90+
// Extended schema sections.
91+
Sections map[string]LayerSection `yaml:",inline,omitempty"`
92+
}
93+
94+
// Section retrieves a section from the plan. Returns nil if
95+
// the field does not exist.
96+
func (p *Plan) Section(field string) LayerSection {
97+
return p.Sections[field]
5098
}
5199

52100
type Layer struct {
@@ -57,6 +105,19 @@ type Layer struct {
57105
Services map[string]*Service `yaml:"services,omitempty"`
58106
Checks map[string]*Check `yaml:"checks,omitempty"`
59107
LogTargets map[string]*LogTarget `yaml:"log-targets,omitempty"`
108+
109+
Sections map[string]LayerSection `yaml:",inline,omitempty"`
110+
}
111+
112+
// addSection adds a new section to the layer.
113+
func (layer *Layer) addSection(field string, section LayerSection) {
114+
layer.Sections[field] = section
115+
}
116+
117+
// Section retrieves a layer section from a layer. Returns nil if
118+
// the field does not exist.
119+
func (layer *Layer) Section(field string) LayerSection {
120+
return layer.Sections[field]
60121
}
61122

62123
type Service struct {
@@ -559,6 +620,7 @@ func CombineLayers(layers ...*Layer) (*Layer, error) {
559620
Services: make(map[string]*Service),
560621
Checks: make(map[string]*Check),
561622
LogTargets: make(map[string]*LogTarget),
623+
Sections: make(map[string]LayerSection),
562624
}
563625
if len(layers) == 0 {
564626
return combined, nil
@@ -643,6 +705,31 @@ func CombineLayers(layers ...*Layer) (*Layer, error) {
643705
}
644706
}
645707

708+
// Combine the same sections from each layer.
709+
for field, extension := range layerExtensions {
710+
var sections []LayerSection
711+
for _, layer := range layers {
712+
if section := layer.Section(field); section != nil {
713+
sections = append(sections, section)
714+
}
715+
}
716+
// Deliberately do not expose the zero section condition to the
717+
// extension. For now, the result of combining nothing must result
718+
// in nothing.
719+
if len(sections) > 0 {
720+
combinedSection, err := extension.CombineSections(sections...)
721+
if err != nil {
722+
return nil, &FormatError{
723+
Message: fmt.Sprintf(`cannot combine section %q: %v`, field, err),
724+
}
725+
}
726+
// We support the ability for a combine to result in nothing.
727+
if combinedSection != nil {
728+
combined.addSection(field, combinedSection)
729+
}
730+
}
731+
}
732+
646733
// Set defaults where required.
647734
for _, service := range combined.Services {
648735
if !service.BackoffDelay.IsSet {
@@ -825,11 +912,18 @@ func (layer *Layer) Validate() error {
825912
}
826913
}
827914

915+
for field, section := range layer.Sections {
916+
err := section.Validate()
917+
if err != nil {
918+
return fmt.Errorf("cannot validate layer section %q: %w", field, err)
919+
}
920+
}
921+
828922
return nil
829923
}
830924

831-
// Validate checks that the combined layers form a valid plan.
832-
// See also Layer.Validate, which checks that the individual layers are valid.
925+
// Validate checks that the combined layers form a valid plan. See also
926+
// Layer.Validate, which checks that the individual layers are valid.
833927
func (p *Plan) Validate() error {
834928
for name, service := range p.Services {
835929
if service.Command == "" {
@@ -917,6 +1011,15 @@ func (p *Plan) Validate() error {
9171011
if err != nil {
9181012
return err
9191013
}
1014+
1015+
// Each section extension must validate the combined plan.
1016+
for field, extension := range layerExtensions {
1017+
err = extension.ValidatePlan(p)
1018+
if err != nil {
1019+
return fmt.Errorf("cannot validate plan section %q: %w", field, err)
1020+
}
1021+
}
1022+
9201023
return nil
9211024
}
9221025

@@ -1020,19 +1123,80 @@ func (p *Plan) checkCycles() error {
10201123
}
10211124

10221125
func ParseLayer(order int, label string, data []byte) (*Layer, error) {
1023-
layer := Layer{
1024-
Services: map[string]*Service{},
1025-
Checks: map[string]*Check{},
1026-
LogTargets: map[string]*LogTarget{},
1027-
}
1028-
dec := yaml.NewDecoder(bytes.NewBuffer(data))
1029-
dec.KnownFields(true)
1030-
err := dec.Decode(&layer)
1126+
layer := &Layer{
1127+
Services: make(map[string]*Service),
1128+
Checks: make(map[string]*Check),
1129+
LogTargets: make(map[string]*LogTarget),
1130+
Sections: make(map[string]LayerSection),
1131+
}
1132+
1133+
// The following manual approach is required because:
1134+
//
1135+
// 1. Extended sections are YAML inlined, and also do not have a
1136+
// concrete type at this level, we cannot simply unmarshal the layer
1137+
// at once.
1138+
//
1139+
// 2. We honor KnownFields = true behaviour for non extended schema
1140+
// sections, and at the top field level, which includes Section field
1141+
// names.
1142+
1143+
builtinSections := map[string]interface{}{
1144+
"summary": &layer.Summary,
1145+
"description": &layer.Description,
1146+
"services": &layer.Services,
1147+
"checks": &layer.Checks,
1148+
"log-targets": &layer.LogTargets,
1149+
}
1150+
1151+
var layerSections map[string]yaml.Node
1152+
err := yaml.Unmarshal(data, &layerSections)
10311153
if err != nil {
10321154
return nil, &FormatError{
10331155
Message: fmt.Sprintf("cannot parse layer %q: %v", label, err),
10341156
}
10351157
}
1158+
1159+
for field, section := range layerSections {
1160+
switch field {
1161+
case "summary", "description", "services", "checks", "log-targets":
1162+
// The following issue prevents us from using the yaml.Node decoder
1163+
// with KnownFields = true behaviour. Once one of the proposals get
1164+
// merged, we can remove the intermediate Marshall step.
1165+
// https://github.com/go-yaml/yaml/issues/460
1166+
data, err := yaml.Marshal(&section)
1167+
if err != nil {
1168+
return nil, fmt.Errorf("internal error: cannot marshal %v section: %w", field, err)
1169+
}
1170+
dec := yaml.NewDecoder(bytes.NewReader(data))
1171+
dec.KnownFields(true)
1172+
if err = dec.Decode(builtinSections[field]); err != nil {
1173+
return nil, &FormatError{
1174+
Message: fmt.Sprintf("cannot parse layer %q section %q: %v", label, field, err),
1175+
}
1176+
}
1177+
default:
1178+
if extension, ok := layerExtensions[field]; ok {
1179+
// Section unmarshal rules are defined by the extension itself.
1180+
extendedSection, err := extension.ParseSection(&section)
1181+
if err != nil {
1182+
return nil, &FormatError{
1183+
Message: fmt.Sprintf("cannot parse layer %q section %q: %v", label, field, err),
1184+
}
1185+
}
1186+
if extendedSection != nil {
1187+
layer.addSection(field, extendedSection)
1188+
}
1189+
} else {
1190+
// At the top level we do not ignore keys we do not understand.
1191+
// This preserves the current Pebble behaviour of decoding with
1192+
// KnownFields = true.
1193+
return nil, &FormatError{
1194+
Message: fmt.Sprintf("cannot parse layer %q: unknown section %q", label, field),
1195+
}
1196+
}
1197+
}
1198+
}
1199+
10361200
layer.Order = order
10371201
layer.Label = label
10381202

@@ -1060,7 +1224,7 @@ func ParseLayer(order int, label string, data []byte) (*Layer, error) {
10601224
return nil, err
10611225
}
10621226

1063-
return &layer, err
1227+
return layer, err
10641228
}
10651229

10661230
func validServiceAction(action ServiceAction, additionalValid ...ServiceAction) bool {
@@ -1164,6 +1328,7 @@ func ReadDir(dir string) (*Plan, error) {
11641328
Services: combined.Services,
11651329
Checks: combined.Checks,
11661330
LogTargets: combined.LogTargets,
1331+
Sections: combined.Sections,
11671332
}
11681333
err = plan.Validate()
11691334
if err != nil {

0 commit comments

Comments
 (0)