@@ -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+
3561const (
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+
4584type 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
52100type 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
62123type 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.
833927func (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
10221125func 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
10661230func 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