55package cni
66
77import (
8- "encoding/json"
9- "errors"
10- "fmt"
11- "log/slog"
12- "os"
13- "path/filepath"
14- "strings"
15-
16- "github.com/containernetworking/cni/pkg/types"
17- type100 "github.com/containernetworking/cni/pkg/types/100"
18-
8+ "go.datum.net/galactic/internal/cnimaster"
199 "go.datum.net/galactic/internal/config"
20- "go.datum.net/galactic/internal/hostconf"
2110)
2211
2312var ConfFile = config .DefaultConfFile
@@ -33,293 +22,11 @@ func InitCNIConfig() {
3322 cniConfig = config .NewCNIConfig ()
3423}
3524
36- const sanitizeForErrorBinary = "<binary>"
37-
38- // errInvalidCNIConfig is the message for CNI config parse errors (code 7).
39- const errInvalidCNIConfig = "invalid CNI config"
40-
41- // errVPCRequired and errVPCAttachmentRequired are messages for missing
42- // identifier fields (code 7).
43- const (
44- errVPCRequired = "vpc is required and must be a non-empty base62 string"
45- errVPCAttachmentRequired = "vpcattachment is required and must be a non-empty base62 string"
46- )
47-
48- // isValidBase62 reports whether s contains only valid base62 characters
49- // ([0-9a-zA-Z]) and is non-empty. VPC and VPCAttachment identifiers are
50- // base62-encoded and used throughout the ADD path (interface naming,
51- // BGP CRD population). Rejecting them early in parseConf prevents cryptic
52- // errors deep in the stack after partial kernel state has been created.
53- func isValidBase62 (s string ) bool {
54- if len (s ) == 0 {
55- return false
56- }
57- for _ , c := range s {
58- if (c < '0' || c > '9' ) && (c < 'a' || c > 'z' ) && (c < 'A' || c > 'Z' ) {
59- return false
60- }
61- }
62- return true
63- }
64-
65- // loadHostConf loads node-local settings from the static per-node conflist.
66- // If the file is missing, it returns a zero-value HostConf (tolerating local
67- // test runs) but still defaulting Namespace to config.DefaultNamespace.
68- func loadHostConf (filePath string ) (* HostConf , error ) {
69- if filePath == "" {
70- filePath = config .DefaultConfFile
71- }
72- conf , err := hostconf .Load (filePath , hostconf .PluginType )
73- if err != nil {
74- if os .IsNotExist (unwrapPathError (err )) {
75- return & HostConf {Namespace : config .DefaultNamespace }, nil
76- }
77- return nil , err
78- }
79- if conf .Namespace == "" {
80- conf .Namespace = config .DefaultNamespace
81- }
82- return conf , nil
83- }
84-
85- // unwrapPathError returns the innermost *os.PathError-shaped error wrapped
86- // by err, if any, so os.IsNotExist (which does not itself traverse %w
87- // wrapping) can still recognize a missing conflist file wrapped by
88- // hostconf.Load's fmt.Errorf("read conflist file %q: %w", ...).
89- func unwrapPathError (err error ) error {
90- for {
91- unwrapped := errors .Unwrap (err )
92- if unwrapped == nil {
93- return err
94- }
95- err = unwrapped
96- }
97- }
98-
99- // parseLogLevel maps a config-supplied level name to a slog.Level. Matching is
100- // case-insensitive. An empty string resolves to config.DefaultLogLevel.
101- // Unrecognized values return an error alongside the info-level fallback, so
102- // callers can warn without failing the CNI operation over a typo'd setting.
103- func parseLogLevel (s string ) (slog.Level , error ) {
104- switch strings .ToLower (strings .TrimSpace (s )) {
105- case "" :
106- return parseLogLevel (config .DefaultLogLevel )
107- case config .LogLevelDebug :
108- return slog .LevelDebug , nil
109- case config .DefaultLogLevel :
110- return slog .LevelInfo , nil
111- case config .LogLevelWarn , config .LogLevelWarning :
112- return slog .LevelWarn , nil
113- case config .LogLevelError :
114- return slog .LevelError , nil
115- default :
116- return slog .LevelInfo , fmt .Errorf ("unknown log level %q (want %s, %s, %s, or %s)" ,
117- s , config .LogLevelDebug , config .DefaultLogLevel , config .LogLevelWarn , config .LogLevelError )
118- }
119- }
120-
121- // setupLogging configures the slog default logger to write to the specified
122- // path at the specified verbosity. If opening the file fails, it logs a
123- // warning to os.Stderr and falls back. An unrecognized logLevel also logs a
124- // warning and falls back to config.DefaultLogLevel rather than failing the
125- // operation.
126- func setupLogging (logPath , logLevel string ) {
127- if logPath == "" {
128- logPath = config .DefaultLogFile
129- }
130- level , err := parseLogLevel (logLevel )
131- if err != nil {
132- slog .Warn ("Invalid log level, falling back to default" ,
133- "value" , logLevel , "default" , config .DefaultLogLevel , "err" , err )
134- }
135- // Ensure parent directory exists.
136- if err := os .MkdirAll (filepath .Dir (logPath ), 0755 ); err != nil {
137- slog .Warn ("Failed to create log directory" , "path" , filepath .Dir (logPath ), "err" , err )
138- return
139- }
140- file , err := os .OpenFile (logPath , os .O_CREATE | os .O_WRONLY | os .O_APPEND , 0644 )
141- if err != nil {
142- slog .Warn ("Failed to open log file, falling back to Stderr" , "path" , logPath , "err" , err )
143- return
144- }
145- // Use JSON handler for structured logging to file.
146- handler := slog .NewJSONHandler (file , & slog.HandlerOptions {Level : level })
147- slog .SetDefault (slog .New (handler ))
148- }
149-
150- // statusConf holds the minimal CNI config fields needed for STATUS validation.
151- //
152- // STATUS only checks that the config is parseable and the API server is
153- // reachable; it does not validate attachment-specific fields (VPC,
154- // VPCAttachment) because STATUS must succeed before any ADD has ever run.
155- type statusConf struct {
156- CNIVersion string `json:"cniVersion"`
157- Type string `json:"type"`
158- }
159-
160- // parseStatusConf validates that the CNI config is parseable and contains the
161- // required top-level fields (cniVersion, type). Unlike parseConf, it does not
162- // validate VPC or VPCAttachment because STATUS must succeed on a freshly
163- // started node before any ADD has run.
164- func parseStatusConf (data []byte ) error {
165- var sc statusConf
166- if err := json .Unmarshal (data , & sc ); err != nil {
167- return & types.Error {Code : 7 , Msg : errInvalidCNIConfig , Details : err .Error ()}
168- }
169- if sc .CNIVersion == "" {
170- return & types.Error {Code : 7 , Msg : "cniVersion is required" }
171- }
172- if sc .Type == "" {
173- return & types.Error {Code : 7 , Msg : "type is required" }
174- }
175- return nil
176- }
177-
178- // validatePrevResult checks that the prevResult (from a preceding plugin in
179- // the CNI chain) is a valid, parseable CNI result. Returns an error if the
180- // result is non-nil but cannot be parsed as a versioned CNI result, ensuring
181- // galactic-cni fails fast rather than silently operating on garbage state.
182- func validatePrevResult (res types.Result ) error {
183- if res == nil {
184- return nil
185- }
186- // Marshal to JSON and re-parse to verify the result is structurally valid.
187- // This catches malformed results that survived CNI framework unmarshaling.
188- jsonBytes , err := json .Marshal (res )
189- if err != nil {
190- return fmt .Errorf ("marshal prevResult: %w" , err )
191- }
192- if _ , err := type100 .NewResult (jsonBytes ); err != nil {
193- return fmt .Errorf ("parse prevResult: %w" , err )
194- }
195- return nil
196- }
197-
198- // validatePrevResultAdd performs content-level validation of prevResult during
199- // the ADD operation. It ensures the preceding plugin produced a result with at
200- // least one interface or IP assignment, which is the minimum expected structure
201- // for any meaningful CNI chain. Returns nil when prevResult is nil (no
202- // preceding plugin) or structurally valid with expected content.
203- func validatePrevResultAdd (res types.Result ) error {
204- if res == nil {
205- return nil
206- }
207- jsonBytes , err := json .Marshal (res )
208- if err != nil {
209- return fmt .Errorf ("marshal prevResult: %w" , err )
210- }
211- result , err := type100 .NewResult (jsonBytes )
212- if err != nil {
213- return fmt .Errorf ("parse prevResult: %w" , err )
214- }
215- versioned , err := type100 .GetResult (result )
216- if err != nil {
217- return fmt .Errorf ("get prevResult version: %w" , err )
218- }
219- // A valid prevResult must declare at least one interface or IP assignment.
220- if len (versioned .Interfaces ) == 0 && len (versioned .IPs ) == 0 {
221- return errors .New ("prevResult declares no interfaces or IP assignments" )
222- }
223- return nil
224- }
225-
226- // parseConf unmarshals the CNI configuration from stdin data and validates
227- // the base62-encoded identifier fields. It resolves the host configuration
228- // and sets up process environment variables and logging.
25+ // parseConf unmarshals the CNI configuration from stdin data, validates the
26+ // base62-encoded identifier fields, and resolves logging. The actual logic
27+ // is shared with galactic-tap-cni — see internal/cnimaster.ParseConf — since
28+ // none of it is veth-specific; this is a thin wrapper binding it to this
29+ // binary's own cniConfig/ConfFile.
22930func parseConf (data []byte ) (* PluginConf , error ) {
230- conf := & PluginConf {}
231- if err := json .Unmarshal (data , & conf ); err != nil {
232- return nil , & types.Error {Code : 7 , Msg : errInvalidCNIConfig , Details : err .Error ()}
233- }
234- if ! isValidBase62 (conf .VPC ) {
235- if len (conf .VPC ) == 0 {
236- return nil , & types.Error {Code : 7 , Msg : errVPCRequired }
237- }
238- return nil , & types.Error {
239- Code : 7 ,
240- Msg : fmt .Sprintf ("invalid base62 value for field 'vpc': %q" , sanitizeForError (conf .VPC )),
241- }
242- }
243- if ! isValidBase62 (conf .VPCAttachment ) {
244- if len (conf .VPCAttachment ) == 0 {
245- return nil , & types.Error {Code : 7 , Msg : errVPCAttachmentRequired }
246- }
247- return nil , & types.Error {
248- Code : 7 ,
249- Msg : fmt .Sprintf ("invalid base62 value for field 'vpcattachment': %q" , sanitizeForError (conf .VPCAttachment )),
250- }
251- }
252-
253- // Load host CNI config
254- hostConf , err := loadHostConf (ConfFile )
255- if err != nil {
256- return nil , fmt .Errorf ("load host CNI config: %w" , err )
257- }
258-
259- // Resolve config: env var > conflist > default.
260- cniConfig .Resolve (& config.ConflistValues {
261- NodeName : hostConf .NodeName ,
262- Kubeconfig : hostConf .Kubeconfig ,
263- Namespace : hostConf .Namespace ,
264- LogFile : hostConf .LogFile ,
265- LogLevel : hostConf .LogLevel ,
266- })
267-
268- // NodeName fallback: auto-detect from the Kubernetes API by matching local
269- // interface addresses against node InternalIPs. This handles cases where
270- // the conflist file is missing (e.g. hostPath mount issues in container-
271- // based environments like Kind).
272- if cniConfig .NodeName == "" {
273- detected , detectErr := hostconf .DetectNodeNameFromAPI ()
274- if detectErr != nil {
275- slog .Warn ("Node name auto-detection failed" , "err" , detectErr )
276- }
277- cniConfig .NodeName = detected
278- }
279- if cniConfig .NodeName == "" {
280- return nil , & types.Error {Code : 4 , Msg : "node name is required (or set GALACTIC_CNI_NODE_NAME)" }
281- }
282- _ = os .Setenv ("NODE_NAME" , cniConfig .NodeName )
283-
284- // Propagate Kubeconfig
285- _ = os .Setenv ("KUBECONFIG" , cniConfig .Kubeconfig )
286-
287- // Resolve and propagate Namespace fallback
288- namespace := conf .Namespace
289- if namespace == "" {
290- namespace = cniConfig .Namespace
291- }
292- conf .Namespace = namespace
293-
294- // Setup Logging
295- setupLogging (cniConfig .LogFile , cniConfig .LogLevel )
296- slog .Debug ("CNI config received" , "stdin" , string (data ))
297-
298- // Whether IPAM runs at all is decided entirely by whether "ipam" is
299- // present — no environment variable or sibling field can trigger or
300- // suppress that. Addressing fields (ipv6_subnet, ipv4_subnet,
301- // address_families, static_ip) and their own default-filling/CIDR
302- // validation live inside internal/cniipam, since they're only ever
303- // read by whichever binary "ipam.type" names — this plugin passes its
304- // own StdinData straight through unmodified when it delegates
305- // (ops_add.go/ops_del.go), so validating them here too would just be
306- // redundant work on the same bytes.
307-
308- if conf .PrevResult != nil {
309- if err := validatePrevResult (conf .PrevResult ); err != nil {
310- return nil , & types.Error {Code : 6 , Msg : fmt .Sprintf ("invalid prevResult: %v" , err )}
311- }
312- }
313- return conf , nil
314- }
315-
316- // sanitizeForError returns s unchanged if it contains only printable ASCII
317- // characters; otherwise returns "<binary>" to avoid corrupting log output.
318- func sanitizeForError (s string ) string {
319- for _ , c := range s {
320- if c < 0x20 || c > 0x7e {
321- return sanitizeForErrorBinary
322- }
323- }
324- return s
31+ return cnimaster .ParseConf (data , cniConfig , ConfFile )
32532}
0 commit comments