Skip to content

Commit 55fe94e

Browse files
committed
invoke: if Result CNIVersion is empty use netconf CNIVersion
The CNI Spec requires plugins to return a CNIVersion in the Result that is the same as the CNIVersion of the incoming CNI config. Empty CNIVersions are specified to map to 0.1.0 (since the first CNI spec didn't have versioning) and libcni handles that automatically. Unfortunately some versions of Weave don't do that and depend on a libcni <= 0.8 quirk that used the netconf version if the result version was empty. This is technically a libcni regression, though the plugin itself is violating the specification. Thus for backwards compatibility assume that if the Result CNIVersion is empty, the netconf CNIVersion should be used as the Result version. Fixes: #895 Signed-off-by: Dan Williams <[email protected]>
1 parent 940e662 commit 55fe94e

File tree

2 files changed

+76
-1
lines changed

2 files changed

+76
-1
lines changed

Diff for: pkg/invoke/exec.go

+44-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package invoke
1616

1717
import (
1818
"context"
19+
"encoding/json"
1920
"fmt"
2021
"os"
2122

@@ -33,6 +34,43 @@ type Exec interface {
3334
Decode(jsonBytes []byte) (version.PluginInfo, error)
3435
}
3536

37+
// Plugin must return result in same version as specified in netconf; but
38+
// for backwards compatibility reasons if the result version is empty use
39+
// config version (rather than technically correct 0.1.0).
40+
// https://github.com/containernetworking/cni/issues/895
41+
func fixupResultVersion(netconf, result []byte) (string, []byte, error) {
42+
versionDecoder := &version.ConfigDecoder{}
43+
confVersion, err := versionDecoder.Decode(netconf)
44+
if err != nil {
45+
return "", nil, err
46+
}
47+
48+
var rawResult map[string]interface{}
49+
if err := json.Unmarshal(result, &rawResult); err != nil {
50+
return "", nil, fmt.Errorf("failed to unmarshal raw result: %w", err)
51+
}
52+
53+
// Manually decode Result version; we need to know whether its cniVersion
54+
// is empty, while built-in decoders (correctly) substitute 0.1.0 for an
55+
// empty version per the CNI spec.
56+
if resultVerRaw, ok := rawResult["cniVersion"]; ok {
57+
resultVer, ok := resultVerRaw.(string)
58+
if ok && resultVer != "" {
59+
return resultVer, result, nil
60+
}
61+
}
62+
63+
// If the cniVersion is not present or empty, assume the result is
64+
// the same CNI spec version as the config
65+
rawResult["cniVersion"] = confVersion
66+
newBytes, err := json.Marshal(rawResult)
67+
if err != nil {
68+
return "", nil, fmt.Errorf("failed to remarshal fixed result: %w", err)
69+
}
70+
71+
return confVersion, newBytes, nil
72+
}
73+
3674
// For example, a testcase could pass an instance of the following fakeExec
3775
// object to ExecPluginWithResult() to verify the incoming stdin and environment
3876
// and provide a tailored response:
@@ -84,7 +122,12 @@ func ExecPluginWithResult(ctx context.Context, pluginPath string, netconf []byte
84122
return nil, err
85123
}
86124

87-
return create.CreateFromBytes(stdoutBytes)
125+
resultVersion, fixedBytes, err := fixupResultVersion(netconf, stdoutBytes)
126+
if err != nil {
127+
return nil, err
128+
}
129+
130+
return create.Create(resultVersion, fixedBytes)
88131
}
89132

90133
func ExecPluginWithoutResult(ctx context.Context, pluginPath string, netconf []byte, args CNIArgs, exec Exec) error {

Diff for: pkg/invoke/exec_test.go

+32
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,38 @@ var _ = Describe("Executing a plugin, unit tests", func() {
9696
_, err := invoke.ExecPluginWithResult(ctx, pluginPath, netconf, cniargs, nil)
9797
Expect(err).To(HaveOccurred())
9898
})
99+
100+
It("assumes config version if result version is missing", func() {
101+
rawExec.ExecPluginCall.Returns.ResultBytes = []byte(`{ "ips": [ { "version": "4", "address": "1.2.3.4/24" } ] }`)
102+
r, err := invoke.ExecPluginWithResult(ctx, pluginPath, netconf, cniargs, pluginExec)
103+
Expect(err).NotTo(HaveOccurred())
104+
Expect(r.Version()).To(Equal("0.3.1"))
105+
106+
result, err := current.GetResult(r)
107+
Expect(err).NotTo(HaveOccurred())
108+
Expect(len(result.IPs)).To(Equal(1))
109+
Expect(result.IPs[0].Address.IP.String()).To(Equal("1.2.3.4"))
110+
})
111+
112+
It("assumes config version if result version is empty", func() {
113+
rawExec.ExecPluginCall.Returns.ResultBytes = []byte(`{ "cniVersion": "", "ips": [ { "version": "4", "address": "1.2.3.4/24" } ] }`)
114+
r, err := invoke.ExecPluginWithResult(ctx, pluginPath, netconf, cniargs, pluginExec)
115+
Expect(err).NotTo(HaveOccurred())
116+
Expect(r.Version()).To(Equal("0.3.1"))
117+
118+
result, err := current.GetResult(r)
119+
Expect(err).NotTo(HaveOccurred())
120+
Expect(len(result.IPs)).To(Equal(1))
121+
Expect(result.IPs[0].Address.IP.String()).To(Equal("1.2.3.4"))
122+
})
123+
124+
It("assumes 0.1.0 if config and result version are empty", func() {
125+
netconf = []byte(`{ "some": "stdin" }`)
126+
rawExec.ExecPluginCall.Returns.ResultBytes = []byte(`{ "some": "version-info" }`)
127+
r, err := invoke.ExecPluginWithResult(ctx, pluginPath, netconf, cniargs, pluginExec)
128+
Expect(err).NotTo(HaveOccurred())
129+
Expect(r.Version()).To(Equal("0.1.0"))
130+
})
99131
})
100132

101133
Describe("without returning a result", func() {

0 commit comments

Comments
 (0)