Bump Kind to v0.24.0 which default node image is Kubernetes v1.31.0.

Signed-off-by: RainbowMango <qdurenhongcai@gmail.com>
This commit is contained in:
RainbowMango 2024-08-29 16:57:09 +08:00
parent 2efe03fac9
commit 9aba233c47
46 changed files with 2602 additions and 878 deletions

8
go.mod
View File

@ -55,7 +55,7 @@ require (
sigs.k8s.io/cluster-api v1.7.1 sigs.k8s.io/cluster-api v1.7.1
sigs.k8s.io/controller-runtime v0.18.4 sigs.k8s.io/controller-runtime v0.18.4
sigs.k8s.io/custom-metrics-apiserver v1.30.0 sigs.k8s.io/custom-metrics-apiserver v1.30.0
sigs.k8s.io/kind v0.22.0 sigs.k8s.io/kind v0.24.0
sigs.k8s.io/mcs-api v0.1.0 sigs.k8s.io/mcs-api v0.1.0
sigs.k8s.io/metrics-server v0.7.1 sigs.k8s.io/metrics-server v0.7.1
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 sigs.k8s.io/structured-merge-diff/v4 v4.4.1
@ -64,10 +64,10 @@ require (
require ( require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/BurntSushi/toml v1.0.0 // indirect github.com/BurntSushi/toml v1.4.0 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect
github.com/alessio/shellescape v1.4.1 // indirect github.com/alessio/shellescape v1.4.2 // indirect
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
@ -129,7 +129,7 @@ require (
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect

20
go.sum
View File

@ -65,8 +65,8 @@ github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxB
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
@ -88,8 +88,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alessio/shellescape v1.2.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/alessio/shellescape v1.2.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0=
github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18=
@ -212,7 +212,6 @@ github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi
github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI=
github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.0.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/evanphx/json-patch/v5 v5.0.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM=
@ -590,7 +589,6 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
@ -619,8 +617,9 @@ github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144T
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
@ -721,7 +720,6 @@ github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4=
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
@ -1381,7 +1379,6 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
@ -1506,8 +1503,8 @@ sigs.k8s.io/custom-metrics-apiserver v1.30.0/go.mod h1:QXOKIL83M545uITzoZn4OC1C7
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/kind v0.8.1/go.mod h1:oNKTxUVPYkV9lWzY6CVMNluVq8cBsyq+UgPJdvA3uu4= sigs.k8s.io/kind v0.8.1/go.mod h1:oNKTxUVPYkV9lWzY6CVMNluVq8cBsyq+UgPJdvA3uu4=
sigs.k8s.io/kind v0.22.0 h1:z/+yr/azoOfzsfooqRsPw1wjJlqT/ukXP0ShkHwNlsI= sigs.k8s.io/kind v0.24.0 h1:g4y4eu0qa+SCeKESLpESgMmVFBebL0BDa6f777OIWrg=
sigs.k8s.io/kind v0.22.0/go.mod h1:aBlbxg08cauDgZ612shr017/rZwqd7AS563FvpWKPVs= sigs.k8s.io/kind v0.24.0/go.mod h1:t7ueEpzPYJvHA8aeLtI52rtFftNgUYUaCwvxjk7phfw=
sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 h1:XX3Ajgzov2RKUdc5jW3t5jwY7Bo7dcRm+tFxT+NfgY0= sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 h1:XX3Ajgzov2RKUdc5jW3t5jwY7Bo7dcRm+tFxT+NfgY0=
sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3/go.mod h1:9n16EZKMhXBNSiUC5kSdFQJkdH3zbxS/JoO619G1VAY= sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3/go.mod h1:9n16EZKMhXBNSiUC5kSdFQJkdH3zbxS/JoO619G1VAY=
sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 h1:W6cLQc5pnqM7vh3b7HvGNfXrJ/xL6BDMS0v1V/HHg5U= sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 h1:W6cLQc5pnqM7vh3b7HvGNfXrJ/xL6BDMS0v1V/HHg5U=
@ -1522,6 +1519,5 @@ sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+s
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

View File

@ -79,7 +79,7 @@ util::verify_go_version
util::verify_docker util::verify_docker
# install kind and kubectl # install kind and kubectl
kind_version=v0.22.0 kind_version=v0.24.0
echo -n "Preparing: 'kind' existence check - " echo -n "Preparing: 'kind' existence check - "
if util::cmd_exist kind; then if util::cmd_exist kind; then
echo "passed" echo "passed"

View File

@ -39,7 +39,7 @@ KARMADA_GO_PACKAGE="github.com/karmada-io/karmada"
MIN_Go_VERSION=go1.22.6 MIN_Go_VERSION=go1.22.6
DEFAULT_CLUSTER_VERSION="kindest/node:v1.27.3" DEFAULT_CLUSTER_VERSION="kindest/node:v1.31.0"
KARMADA_TARGET_SOURCE=( KARMADA_TARGET_SOURCE=(
karmada-aggregated-apiserver=cmd/aggregated-apiserver karmada-aggregated-apiserver=cmd/aggregated-apiserver

View File

@ -1,2 +1,2 @@
toml.test /toml.test
/toml-test /toml-test

View File

@ -1 +0,0 @@
Compatible with TOML version [v1.0.0](https://toml.io/en/v1.0.0).

View File

@ -1,6 +1,5 @@
TOML stands for Tom's Obvious, Minimal Language. This Go package provides a TOML stands for Tom's Obvious, Minimal Language. This Go package provides a
reflection interface similar to Go's standard library `json` and `xml` reflection interface similar to Go's standard library `json` and `xml` packages.
packages.
Compatible with TOML version [v1.0.0](https://toml.io/en/v1.0.0). Compatible with TOML version [v1.0.0](https://toml.io/en/v1.0.0).
@ -10,7 +9,7 @@ See the [releases page](https://github.com/BurntSushi/toml/releases) for a
changelog; this information is also in the git tag annotations (e.g. `git show changelog; this information is also in the git tag annotations (e.g. `git show
v0.4.0`). v0.4.0`).
This library requires Go 1.13 or newer; install it with: This library requires Go 1.18 or newer; add it to your go.mod with:
% go get github.com/BurntSushi/toml@latest % go get github.com/BurntSushi/toml@latest
@ -19,16 +18,7 @@ It also comes with a TOML validator CLI tool:
% go install github.com/BurntSushi/toml/cmd/tomlv@latest % go install github.com/BurntSushi/toml/cmd/tomlv@latest
% tomlv some-toml-file.toml % tomlv some-toml-file.toml
### Testing
This package passes all tests in [toml-test] for both the decoder and the
encoder.
[toml-test]: https://github.com/BurntSushi/toml-test
### Examples ### Examples
This package works similar to how the Go standard library handles XML and JSON.
Namely, data is loaded into Go values via reflection.
For the simplest example, consider some TOML file as just a list of keys and For the simplest example, consider some TOML file as just a list of keys and
values: values:
@ -40,7 +30,7 @@ Perfection = [ 6, 28, 496, 8128 ]
DOB = 1987-07-05T05:45:00Z DOB = 1987-07-05T05:45:00Z
``` ```
Which could be defined in Go as: Which can be decoded with:
```go ```go
type Config struct { type Config struct {
@ -48,20 +38,15 @@ type Config struct {
Cats []string Cats []string
Pi float64 Pi float64
Perfection []int Perfection []int
DOB time.Time // requires `import time` DOB time.Time
} }
```
And then decoded with:
```go
var conf Config var conf Config
err := toml.Decode(tomlData, &conf) _, err := toml.Decode(tomlData, &conf)
// handle error
``` ```
You can also use struct tags if your struct field name doesn't map to a TOML You can also use struct tags if your struct field name doesn't map to a TOML key
key value directly: value directly:
```toml ```toml
some_key_NAME = "wat" some_key_NAME = "wat"
@ -73,139 +58,63 @@ type TOML struct {
} }
``` ```
Beware that like other most other decoders **only exported fields** are Beware that like other decoders **only exported fields** are considered when
considered when encoding and decoding; private fields are silently ignored. encoding and decoding; private fields are silently ignored.
### Using the `Marshaler` and `encoding.TextUnmarshaler` interfaces ### Using the `Marshaler` and `encoding.TextUnmarshaler` interfaces
Here's an example that automatically parses duration strings into Here's an example that automatically parses values in a `mail.Address`:
`time.Duration` values:
```toml ```toml
[[song]] contacts = [
name = "Thunder Road" "Donald Duck <donald@duckburg.com>",
duration = "4m49s" "Scrooge McDuck <scrooge@duckburg.com>",
]
[[song]]
name = "Stairway to Heaven"
duration = "8m03s"
``` ```
Which can be decoded with: Can be decoded with:
```go ```go
type song struct { // Create address type which satisfies the encoding.TextUnmarshaler interface.
Name string type address struct {
Duration duration *mail.Address
}
type songs struct {
Song []song
}
var favorites songs
if _, err := toml.Decode(blob, &favorites); err != nil {
log.Fatal(err)
} }
for _, s := range favorites.Song { func (a *address) UnmarshalText(text []byte) error {
fmt.Printf("%s (%s)\n", s.Name, s.Duration)
}
```
And you'll also need a `duration` type that satisfies the
`encoding.TextUnmarshaler` interface:
```go
type duration struct {
time.Duration
}
func (d *duration) UnmarshalText(text []byte) error {
var err error var err error
d.Duration, err = time.ParseDuration(string(text)) a.Address, err = mail.ParseAddress(string(text))
return err return err
} }
// Decode it.
func decode() {
blob := `
contacts = [
"Donald Duck <donald@duckburg.com>",
"Scrooge McDuck <scrooge@duckburg.com>",
]
`
var contacts struct {
Contacts []address
}
_, err := toml.Decode(blob, &contacts)
if err != nil {
log.Fatal(err)
}
for _, c := range contacts.Contacts {
fmt.Printf("%#v\n", c.Address)
}
// Output:
// &mail.Address{Name:"Donald Duck", Address:"donald@duckburg.com"}
// &mail.Address{Name:"Scrooge McDuck", Address:"scrooge@duckburg.com"}
}
``` ```
To target TOML specifically you can implement `UnmarshalTOML` TOML interface in To target TOML specifically you can implement `UnmarshalTOML` TOML interface in
a similar way. a similar way.
### More complex usage ### More complex usage
Here's an example of how to load the example from the official spec page: See the [`_example/`](/_example) directory for a more complex example.
```toml
# This is a TOML document. Boom.
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
organization = "GitHub"
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
dob = 1979-05-27T07:32:00Z # First class dates? Why not?
[database]
server = "192.168.1.1"
ports = [ 8001, 8001, 8002 ]
connection_max = 5000
enabled = true
[servers]
# You can indent as you please. Tabs or spaces. TOML don't care.
[servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"
[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"
[clients]
data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
# Line breaks are OK when inside arrays
hosts = [
"alpha",
"omega"
]
```
And the corresponding Go types are:
```go
type tomlConfig struct {
Title string
Owner ownerInfo
DB database `toml:"database"`
Servers map[string]server
Clients clients
}
type ownerInfo struct {
Name string
Org string `toml:"organization"`
Bio string
DOB time.Time
}
type database struct {
Server string
Ports []int
ConnMax int `toml:"connection_max"`
Enabled bool
}
type server struct {
IP string
DC string
}
type clients struct {
Data [][]interface{}
Hosts []string
}
```
Note that a case insensitive match will be tried if an exact match can't be
found.
A working example of the above can be found in `_example/example.{go,toml}`.

View File

@ -1,32 +1,66 @@
package toml package toml
import ( import (
"bytes"
"encoding" "encoding"
"encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/fs"
"math" "math"
"os" "os"
"reflect" "reflect"
"strconv"
"strings" "strings"
"time"
) )
// Unmarshaler is the interface implemented by objects that can unmarshal a // Unmarshaler is the interface implemented by objects that can unmarshal a
// TOML description of themselves. // TOML description of themselves.
type Unmarshaler interface { type Unmarshaler interface {
UnmarshalTOML(interface{}) error UnmarshalTOML(any) error
} }
// Unmarshal decodes the contents of `p` in TOML format into a pointer `v`. // Unmarshal decodes the contents of data in TOML format into a pointer v.
func Unmarshal(p []byte, v interface{}) error { //
_, err := Decode(string(p), v) // See [Decoder] for a description of the decoding process.
func Unmarshal(data []byte, v any) error {
_, err := NewDecoder(bytes.NewReader(data)).Decode(v)
return err return err
} }
// Decode the TOML data in to the pointer v.
//
// See [Decoder] for a description of the decoding process.
func Decode(data string, v any) (MetaData, error) {
return NewDecoder(strings.NewReader(data)).Decode(v)
}
// DecodeFile reads the contents of a file and decodes it with [Decode].
func DecodeFile(path string, v any) (MetaData, error) {
fp, err := os.Open(path)
if err != nil {
return MetaData{}, err
}
defer fp.Close()
return NewDecoder(fp).Decode(v)
}
// DecodeFS reads the contents of a file from [fs.FS] and decodes it with
// [Decode].
func DecodeFS(fsys fs.FS, path string, v any) (MetaData, error) {
fp, err := fsys.Open(path)
if err != nil {
return MetaData{}, err
}
defer fp.Close()
return NewDecoder(fp).Decode(v)
}
// Primitive is a TOML value that hasn't been decoded into a Go value. // Primitive is a TOML value that hasn't been decoded into a Go value.
// //
// This type can be used for any value, which will cause decoding to be delayed. // This type can be used for any value, which will cause decoding to be delayed.
// You can use the PrimitiveDecode() function to "manually" decode these values. // You can use [PrimitiveDecode] to "manually" decode these values.
// //
// NOTE: The underlying representation of a `Primitive` value is subject to // NOTE: The underlying representation of a `Primitive` value is subject to
// change. Do not rely on it. // change. Do not rely on it.
@ -35,43 +69,29 @@ func Unmarshal(p []byte, v interface{}) error {
// overhead of reflection. They can be useful when you don't know the exact type // overhead of reflection. They can be useful when you don't know the exact type
// of TOML data until runtime. // of TOML data until runtime.
type Primitive struct { type Primitive struct {
undecoded interface{} undecoded any
context Key context Key
} }
// The significand precision for float32 and float64 is 24 and 53 bits; this is // The significand precision for float32 and float64 is 24 and 53 bits; this is
// the range a natural number can be stored in a float without loss of data. // the range a natural number can be stored in a float without loss of data.
const ( const (
maxSafeFloat32Int = 16777215 // 2^24-1 maxSafeFloat32Int = 16777215 // 2^24-1
maxSafeFloat64Int = 9007199254740991 // 2^53-1 maxSafeFloat64Int = int64(9007199254740991) // 2^53-1
) )
// PrimitiveDecode is just like the other `Decode*` functions, except it
// decodes a TOML value that has already been parsed. Valid primitive values
// can *only* be obtained from values filled by the decoder functions,
// including this method. (i.e., `v` may contain more `Primitive`
// values.)
//
// Meta data for primitive values is included in the meta data returned by
// the `Decode*` functions with one exception: keys returned by the Undecoded
// method will only reflect keys that were decoded. Namely, any keys hidden
// behind a Primitive will be considered undecoded. Executing this method will
// update the undecoded keys in the meta data. (See the example.)
func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error {
md.context = primValue.context
defer func() { md.context = nil }()
return md.unify(primValue.undecoded, rvalue(v))
}
// Decoder decodes TOML data. // Decoder decodes TOML data.
// //
// TOML tables correspond to Go structs or maps (dealer's choice they can be // TOML tables correspond to Go structs or maps; they can be used
// used interchangeably). // interchangeably, but structs offer better type safety.
// //
// TOML table arrays correspond to either a slice of structs or a slice of maps. // TOML table arrays correspond to either a slice of structs or a slice of maps.
// //
// TOML datetimes correspond to Go time.Time values. Local datetimes are parsed // TOML datetimes correspond to [time.Time]. Local datetimes are parsed in the
// in the local timezone. // local timezone.
//
// [time.Duration] types are treated as nanoseconds if the TOML value is an
// integer, or they're parsed with time.ParseDuration() if they're strings.
// //
// All other TOML types (float, string, int, bool and array) correspond to the // All other TOML types (float, string, int, bool and array) correspond to the
// obvious Go types. // obvious Go types.
@ -80,9 +100,9 @@ func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error {
// interface, in which case any primitive TOML value (floats, strings, integers, // interface, in which case any primitive TOML value (floats, strings, integers,
// booleans, datetimes) will be converted to a []byte and given to the value's // booleans, datetimes) will be converted to a []byte and given to the value's
// UnmarshalText method. See the Unmarshaler example for a demonstration with // UnmarshalText method. See the Unmarshaler example for a demonstration with
// time duration strings. // email addresses.
// //
// Key mapping // # Key mapping
// //
// TOML keys can map to either keys in a Go map or field names in a Go struct. // TOML keys can map to either keys in a Go map or field names in a Go struct.
// The special `toml` struct tag can be used to map TOML keys to struct fields // The special `toml` struct tag can be used to map TOML keys to struct fields
@ -109,10 +129,11 @@ func NewDecoder(r io.Reader) *Decoder {
var ( var (
unmarshalToml = reflect.TypeOf((*Unmarshaler)(nil)).Elem() unmarshalToml = reflect.TypeOf((*Unmarshaler)(nil)).Elem()
unmarshalText = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() unmarshalText = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
primitiveType = reflect.TypeOf((*Primitive)(nil)).Elem()
) )
// Decode TOML data in to the pointer `v`. // Decode TOML data in to the pointer `v`.
func (dec *Decoder) Decode(v interface{}) (MetaData, error) { func (dec *Decoder) Decode(v any) (MetaData, error) {
rv := reflect.ValueOf(v) rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr { if rv.Kind() != reflect.Ptr {
s := "%q" s := "%q"
@ -120,25 +141,25 @@ func (dec *Decoder) Decode(v interface{}) (MetaData, error) {
s = "%v" s = "%v"
} }
return MetaData{}, e("cannot decode to non-pointer "+s, reflect.TypeOf(v)) return MetaData{}, fmt.Errorf("toml: cannot decode to non-pointer "+s, reflect.TypeOf(v))
} }
if rv.IsNil() { if rv.IsNil() {
return MetaData{}, e("cannot decode to nil value of %q", reflect.TypeOf(v)) return MetaData{}, fmt.Errorf("toml: cannot decode to nil value of %q", reflect.TypeOf(v))
} }
// Check if this is a supported type: struct, map, interface{}, or something // Check if this is a supported type: struct, map, any, or something that
// that implements UnmarshalTOML or UnmarshalText. // implements UnmarshalTOML or UnmarshalText.
rv = indirect(rv) rv = indirect(rv)
rt := rv.Type() rt := rv.Type()
if rv.Kind() != reflect.Struct && rv.Kind() != reflect.Map && if rv.Kind() != reflect.Struct && rv.Kind() != reflect.Map &&
!(rv.Kind() == reflect.Interface && rv.NumMethod() == 0) && !(rv.Kind() == reflect.Interface && rv.NumMethod() == 0) &&
!rt.Implements(unmarshalToml) && !rt.Implements(unmarshalText) { !rt.Implements(unmarshalToml) && !rt.Implements(unmarshalText) {
return MetaData{}, e("cannot decode to type %s", rt) return MetaData{}, fmt.Errorf("toml: cannot decode to type %s", rt)
} }
// TODO: parser should read from io.Reader? Or at the very least, make it // TODO: parser should read from io.Reader? Or at the very least, make it
// read from []byte rather than string // read from []byte rather than string
data, err := ioutil.ReadAll(dec.r) data, err := io.ReadAll(dec.r)
if err != nil { if err != nil {
return MetaData{}, err return MetaData{}, err
} }
@ -150,30 +171,29 @@ func (dec *Decoder) Decode(v interface{}) (MetaData, error) {
md := MetaData{ md := MetaData{
mapping: p.mapping, mapping: p.mapping,
types: p.types, keyInfo: p.keyInfo,
keys: p.ordered, keys: p.ordered,
decoded: make(map[string]struct{}, len(p.ordered)), decoded: make(map[string]struct{}, len(p.ordered)),
context: nil, context: nil,
data: data,
} }
return md, md.unify(p.mapping, rv) return md, md.unify(p.mapping, rv)
} }
// Decode the TOML data in to the pointer v. // PrimitiveDecode is just like the other Decode* functions, except it decodes a
// TOML value that has already been parsed. Valid primitive values can *only* be
// obtained from values filled by the decoder functions, including this method.
// (i.e., v may contain more [Primitive] values.)
// //
// See the documentation on Decoder for a description of the decoding process. // Meta data for primitive values is included in the meta data returned by the
func Decode(data string, v interface{}) (MetaData, error) { // Decode* functions with one exception: keys returned by the Undecoded method
return NewDecoder(strings.NewReader(data)).Decode(v) // will only reflect keys that were decoded. Namely, any keys hidden behind a
} // Primitive will be considered undecoded. Executing this method will update the
// undecoded keys in the meta data. (See the example.)
// DecodeFile is just like Decode, except it will automatically read the func (md *MetaData) PrimitiveDecode(primValue Primitive, v any) error {
// contents of the file at path and decode it for you. md.context = primValue.context
func DecodeFile(path string, v interface{}) (MetaData, error) { defer func() { md.context = nil }()
fp, err := os.Open(path) return md.unify(primValue.undecoded, rvalue(v))
if err != nil {
return MetaData{}, err
}
defer fp.Close()
return NewDecoder(fp).Decode(v)
} }
// unify performs a sort of type unification based on the structure of `rv`, // unify performs a sort of type unification based on the structure of `rv`,
@ -181,10 +201,10 @@ func DecodeFile(path string, v interface{}) (MetaData, error) {
// //
// Any type mismatch produces an error. Finding a type that we don't know // Any type mismatch produces an error. Finding a type that we don't know
// how to handle produces an unsupported type error. // how to handle produces an unsupported type error.
func (md *MetaData) unify(data interface{}, rv reflect.Value) error { func (md *MetaData) unify(data any, rv reflect.Value) error {
// Special case. Look for a `Primitive` value. // Special case. Look for a `Primitive` value.
// TODO: #76 would make this superfluous after implemented. // TODO: #76 would make this superfluous after implemented.
if rv.Type() == reflect.TypeOf((*Primitive)(nil)).Elem() { if rv.Type() == primitiveType {
// Save the undecoded data and the key context into the primitive // Save the undecoded data and the key context into the primitive
// value. // value.
context := make(Key, len(md.context)) context := make(Key, len(md.context))
@ -196,17 +216,18 @@ func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
return nil return nil
} }
// Special case. Unmarshaler Interface support. rvi := rv.Interface()
if rv.CanAddr() { if v, ok := rvi.(Unmarshaler); ok {
if v, ok := rv.Addr().Interface().(Unmarshaler); ok { err := v.UnmarshalTOML(data)
return v.UnmarshalTOML(data) if err != nil {
return md.parseErr(err)
} }
return nil
} }
if v, ok := rvi.(encoding.TextUnmarshaler); ok {
// Special case. Look for a value satisfying the TextUnmarshaler interface.
if v, ok := rv.Interface().(encoding.TextUnmarshaler); ok {
return md.unifyText(data, v) return md.unifyText(data, v)
} }
// TODO: // TODO:
// The behavior here is incorrect whenever a Go type satisfies the // The behavior here is incorrect whenever a Go type satisfies the
// encoding.TextUnmarshaler interface but also corresponds to a TOML hash or // encoding.TextUnmarshaler interface but also corresponds to a TOML hash or
@ -217,19 +238,10 @@ func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
k := rv.Kind() k := rv.Kind()
// laziness
if k >= reflect.Int && k <= reflect.Uint64 { if k >= reflect.Int && k <= reflect.Uint64 {
return md.unifyInt(data, rv) return md.unifyInt(data, rv)
} }
switch k { switch k {
case reflect.Ptr:
elem := reflect.New(rv.Type().Elem())
err := md.unify(data, reflect.Indirect(elem))
if err != nil {
return err
}
rv.Set(elem)
return nil
case reflect.Struct: case reflect.Struct:
return md.unifyStruct(data, rv) return md.unifyStruct(data, rv)
case reflect.Map: case reflect.Map:
@ -243,25 +255,23 @@ func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
case reflect.Bool: case reflect.Bool:
return md.unifyBool(data, rv) return md.unifyBool(data, rv)
case reflect.Interface: case reflect.Interface:
// we only support empty interfaces. if rv.NumMethod() > 0 { /// Only empty interfaces are supported.
if rv.NumMethod() > 0 { return md.e("unsupported type %s", rv.Type())
return e("unsupported type %s", rv.Type())
} }
return md.unifyAnything(data, rv) return md.unifyAnything(data, rv)
case reflect.Float32, reflect.Float64: case reflect.Float32, reflect.Float64:
return md.unifyFloat64(data, rv) return md.unifyFloat64(data, rv)
} }
return e("unsupported type %s", rv.Kind()) return md.e("unsupported type %s", rv.Kind())
} }
func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error { func (md *MetaData) unifyStruct(mapping any, rv reflect.Value) error {
tmap, ok := mapping.(map[string]interface{}) tmap, ok := mapping.(map[string]any)
if !ok { if !ok {
if mapping == nil { if mapping == nil {
return nil return nil
} }
return e("type mismatch for %s: expected table but found %T", return md.e("type mismatch for %s: expected table but found %s", rv.Type().String(), fmtType(mapping))
rv.Type().String(), mapping)
} }
for key, datum := range tmap { for key, datum := range tmap {
@ -286,27 +296,28 @@ func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
if isUnifiable(subv) { if isUnifiable(subv) {
md.decoded[md.context.add(key).String()] = struct{}{} md.decoded[md.context.add(key).String()] = struct{}{}
md.context = append(md.context, key) md.context = append(md.context, key)
err := md.unify(datum, subv) err := md.unify(datum, subv)
if err != nil { if err != nil {
return err return err
} }
md.context = md.context[0 : len(md.context)-1] md.context = md.context[0 : len(md.context)-1]
} else if f.name != "" { } else if f.name != "" {
return e("cannot write unexported field %s.%s", rv.Type().String(), f.name) return md.e("cannot write unexported field %s.%s", rv.Type().String(), f.name)
} }
} }
} }
return nil return nil
} }
func (md *MetaData) unifyMap(mapping interface{}, rv reflect.Value) error { func (md *MetaData) unifyMap(mapping any, rv reflect.Value) error {
if k := rv.Type().Key().Kind(); k != reflect.String { keyType := rv.Type().Key().Kind()
return fmt.Errorf( if keyType != reflect.String && keyType != reflect.Interface {
"toml: cannot decode to a map with non-string key type (%s in %q)", return fmt.Errorf("toml: cannot decode to a map with non-string key type (%s in %q)",
k, rv.Type()) keyType, rv.Type())
} }
tmap, ok := mapping.(map[string]interface{}) tmap, ok := mapping.(map[string]any)
if !ok { if !ok {
if tmap == nil { if tmap == nil {
return nil return nil
@ -321,19 +332,28 @@ func (md *MetaData) unifyMap(mapping interface{}, rv reflect.Value) error {
md.context = append(md.context, k) md.context = append(md.context, k)
rvval := reflect.Indirect(reflect.New(rv.Type().Elem())) rvval := reflect.Indirect(reflect.New(rv.Type().Elem()))
if err := md.unify(v, rvval); err != nil {
err := md.unify(v, indirect(rvval))
if err != nil {
return err return err
} }
md.context = md.context[0 : len(md.context)-1] md.context = md.context[0 : len(md.context)-1]
rvkey := indirect(reflect.New(rv.Type().Key())) rvkey := indirect(reflect.New(rv.Type().Key()))
rvkey.SetString(k)
switch keyType {
case reflect.Interface:
rvkey.Set(reflect.ValueOf(k))
case reflect.String:
rvkey.SetString(k)
}
rv.SetMapIndex(rvkey, rvval) rv.SetMapIndex(rvkey, rvval)
} }
return nil return nil
} }
func (md *MetaData) unifyArray(data interface{}, rv reflect.Value) error { func (md *MetaData) unifyArray(data any, rv reflect.Value) error {
datav := reflect.ValueOf(data) datav := reflect.ValueOf(data)
if datav.Kind() != reflect.Slice { if datav.Kind() != reflect.Slice {
if !datav.IsValid() { if !datav.IsValid() {
@ -342,12 +362,12 @@ func (md *MetaData) unifyArray(data interface{}, rv reflect.Value) error {
return md.badtype("slice", data) return md.badtype("slice", data)
} }
if l := datav.Len(); l != rv.Len() { if l := datav.Len(); l != rv.Len() {
return e("expected array length %d; got TOML array of length %d", rv.Len(), l) return md.e("expected array length %d; got TOML array of length %d", rv.Len(), l)
} }
return md.unifySliceArray(datav, rv) return md.unifySliceArray(datav, rv)
} }
func (md *MetaData) unifySlice(data interface{}, rv reflect.Value) error { func (md *MetaData) unifySlice(data any, rv reflect.Value) error {
datav := reflect.ValueOf(data) datav := reflect.ValueOf(data)
if datav.Kind() != reflect.Slice { if datav.Kind() != reflect.Slice {
if !datav.IsValid() { if !datav.IsValid() {
@ -374,7 +394,19 @@ func (md *MetaData) unifySliceArray(data, rv reflect.Value) error {
return nil return nil
} }
func (md *MetaData) unifyString(data interface{}, rv reflect.Value) error { func (md *MetaData) unifyString(data any, rv reflect.Value) error {
_, ok := rv.Interface().(json.Number)
if ok {
if i, ok := data.(int64); ok {
rv.SetString(strconv.FormatInt(i, 10))
} else if f, ok := data.(float64); ok {
rv.SetString(strconv.FormatFloat(f, 'f', -1, 64))
} else {
return md.badtype("string", data)
}
return nil
}
if s, ok := data.(string); ok { if s, ok := data.(string); ok {
rv.SetString(s) rv.SetString(s)
return nil return nil
@ -382,12 +414,14 @@ func (md *MetaData) unifyString(data interface{}, rv reflect.Value) error {
return md.badtype("string", data) return md.badtype("string", data)
} }
func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error { func (md *MetaData) unifyFloat64(data any, rv reflect.Value) error {
rvk := rv.Kind()
if num, ok := data.(float64); ok { if num, ok := data.(float64); ok {
switch rv.Kind() { switch rvk {
case reflect.Float32: case reflect.Float32:
if num < -math.MaxFloat32 || num > math.MaxFloat32 { if num < -math.MaxFloat32 || num > math.MaxFloat32 {
return e("value %f is out of range for float32", num) return md.parseErr(errParseRange{i: num, size: rvk.String()})
} }
fallthrough fallthrough
case reflect.Float64: case reflect.Float64:
@ -399,74 +433,61 @@ func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error {
} }
if num, ok := data.(int64); ok { if num, ok := data.(int64); ok {
switch rv.Kind() { if (rvk == reflect.Float32 && (num < -maxSafeFloat32Int || num > maxSafeFloat32Int)) ||
case reflect.Float32: (rvk == reflect.Float64 && (num < -maxSafeFloat64Int || num > maxSafeFloat64Int)) {
if num < -maxSafeFloat32Int || num > maxSafeFloat32Int { return md.parseErr(errUnsafeFloat{i: num, size: rvk.String()})
return e("value %d is out of range for float32", num)
}
fallthrough
case reflect.Float64:
if num < -maxSafeFloat64Int || num > maxSafeFloat64Int {
return e("value %d is out of range for float64", num)
}
rv.SetFloat(float64(num))
default:
panic("bug")
} }
rv.SetFloat(float64(num))
return nil return nil
} }
return md.badtype("float", data) return md.badtype("float", data)
} }
func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error { func (md *MetaData) unifyInt(data any, rv reflect.Value) error {
if num, ok := data.(int64); ok { _, ok := rv.Interface().(time.Duration)
if rv.Kind() >= reflect.Int && rv.Kind() <= reflect.Int64 { if ok {
switch rv.Kind() { // Parse as string duration, and fall back to regular integer parsing
case reflect.Int, reflect.Int64: // (as nanosecond) if this is not a string.
// No bounds checking necessary. if s, ok := data.(string); ok {
case reflect.Int8: dur, err := time.ParseDuration(s)
if num < math.MinInt8 || num > math.MaxInt8 { if err != nil {
return e("value %d is out of range for int8", num) return md.parseErr(errParseDuration{s})
}
case reflect.Int16:
if num < math.MinInt16 || num > math.MaxInt16 {
return e("value %d is out of range for int16", num)
}
case reflect.Int32:
if num < math.MinInt32 || num > math.MaxInt32 {
return e("value %d is out of range for int32", num)
}
} }
rv.SetInt(num) rv.SetInt(int64(dur))
} else if rv.Kind() >= reflect.Uint && rv.Kind() <= reflect.Uint64 { return nil
unum := uint64(num)
switch rv.Kind() {
case reflect.Uint, reflect.Uint64:
// No bounds checking necessary.
case reflect.Uint8:
if num < 0 || unum > math.MaxUint8 {
return e("value %d is out of range for uint8", num)
}
case reflect.Uint16:
if num < 0 || unum > math.MaxUint16 {
return e("value %d is out of range for uint16", num)
}
case reflect.Uint32:
if num < 0 || unum > math.MaxUint32 {
return e("value %d is out of range for uint32", num)
}
}
rv.SetUint(unum)
} else {
panic("unreachable")
} }
return nil
} }
return md.badtype("integer", data)
num, ok := data.(int64)
if !ok {
return md.badtype("integer", data)
}
rvk := rv.Kind()
switch {
case rvk >= reflect.Int && rvk <= reflect.Int64:
if (rvk == reflect.Int8 && (num < math.MinInt8 || num > math.MaxInt8)) ||
(rvk == reflect.Int16 && (num < math.MinInt16 || num > math.MaxInt16)) ||
(rvk == reflect.Int32 && (num < math.MinInt32 || num > math.MaxInt32)) {
return md.parseErr(errParseRange{i: num, size: rvk.String()})
}
rv.SetInt(num)
case rvk >= reflect.Uint && rvk <= reflect.Uint64:
unum := uint64(num)
if rvk == reflect.Uint8 && (num < 0 || unum > math.MaxUint8) ||
rvk == reflect.Uint16 && (num < 0 || unum > math.MaxUint16) ||
rvk == reflect.Uint32 && (num < 0 || unum > math.MaxUint32) {
return md.parseErr(errParseRange{i: num, size: rvk.String()})
}
rv.SetUint(unum)
default:
panic("unreachable")
}
return nil
} }
func (md *MetaData) unifyBool(data interface{}, rv reflect.Value) error { func (md *MetaData) unifyBool(data any, rv reflect.Value) error {
if b, ok := data.(bool); ok { if b, ok := data.(bool); ok {
rv.SetBool(b) rv.SetBool(b)
return nil return nil
@ -474,12 +495,12 @@ func (md *MetaData) unifyBool(data interface{}, rv reflect.Value) error {
return md.badtype("boolean", data) return md.badtype("boolean", data)
} }
func (md *MetaData) unifyAnything(data interface{}, rv reflect.Value) error { func (md *MetaData) unifyAnything(data any, rv reflect.Value) error {
rv.Set(reflect.ValueOf(data)) rv.Set(reflect.ValueOf(data))
return nil return nil
} }
func (md *MetaData) unifyText(data interface{}, v encoding.TextUnmarshaler) error { func (md *MetaData) unifyText(data any, v encoding.TextUnmarshaler) error {
var s string var s string
switch sdata := data.(type) { switch sdata := data.(type) {
case Marshaler: case Marshaler:
@ -488,7 +509,7 @@ func (md *MetaData) unifyText(data interface{}, v encoding.TextUnmarshaler) erro
return err return err
} }
s = string(text) s = string(text)
case TextMarshaler: case encoding.TextMarshaler:
text, err := sdata.MarshalText() text, err := sdata.MarshalText()
if err != nil { if err != nil {
return err return err
@ -508,17 +529,40 @@ func (md *MetaData) unifyText(data interface{}, v encoding.TextUnmarshaler) erro
return md.badtype("primitive (string-like)", data) return md.badtype("primitive (string-like)", data)
} }
if err := v.UnmarshalText([]byte(s)); err != nil { if err := v.UnmarshalText([]byte(s)); err != nil {
return err return md.parseErr(err)
} }
return nil return nil
} }
func (md *MetaData) badtype(dst string, data interface{}) error { func (md *MetaData) badtype(dst string, data any) error {
return e("incompatible types: TOML key %q has type %T; destination has type %s", md.context, data, dst) return md.e("incompatible types: TOML value has type %s; destination has type %s", fmtType(data), dst)
}
func (md *MetaData) parseErr(err error) error {
k := md.context.String()
return ParseError{
LastKey: k,
Position: md.keyInfo[k].pos,
Line: md.keyInfo[k].pos.Line,
err: err,
input: string(md.data),
}
}
func (md *MetaData) e(format string, args ...any) error {
f := "toml: "
if len(md.context) > 0 {
f = fmt.Sprintf("toml: (last key %q): ", md.context)
p := md.keyInfo[md.context.String()].pos
if p.Line > 0 {
f = fmt.Sprintf("toml: line %d (last key %q): ", p.Line, md.context)
}
}
return fmt.Errorf(f+format, args...)
} }
// rvalue returns a reflect.Value of `v`. All pointers are resolved. // rvalue returns a reflect.Value of `v`. All pointers are resolved.
func rvalue(v interface{}) reflect.Value { func rvalue(v any) reflect.Value {
return indirect(reflect.ValueOf(v)) return indirect(reflect.ValueOf(v))
} }
@ -533,7 +577,11 @@ func indirect(v reflect.Value) reflect.Value {
if v.Kind() != reflect.Ptr { if v.Kind() != reflect.Ptr {
if v.CanSet() { if v.CanSet() {
pv := v.Addr() pv := v.Addr()
if _, ok := pv.Interface().(encoding.TextUnmarshaler); ok { pvi := pv.Interface()
if _, ok := pvi.(encoding.TextUnmarshaler); ok {
return pv
}
if _, ok := pvi.(Unmarshaler); ok {
return pv return pv
} }
} }
@ -549,12 +597,17 @@ func isUnifiable(rv reflect.Value) bool {
if rv.CanSet() { if rv.CanSet() {
return true return true
} }
if _, ok := rv.Interface().(encoding.TextUnmarshaler); ok { rvi := rv.Interface()
if _, ok := rvi.(encoding.TextUnmarshaler); ok {
return true
}
if _, ok := rvi.(Unmarshaler); ok {
return true return true
} }
return false return false
} }
func e(format string, args ...interface{}) error { // fmt %T with "interface {}" replaced with "any", which is far more readable.
return fmt.Errorf("toml: "+format, args...) func fmtType(t any) string {
return strings.ReplaceAll(fmt.Sprintf("%T", t), "interface {}", "any")
} }

View File

@ -1,19 +0,0 @@
//go:build go1.16
// +build go1.16
package toml
import (
"io/fs"
)
// DecodeFS is just like Decode, except it will automatically read the contents
// of the file at `path` from a fs.FS instance.
func DecodeFS(fsys fs.FS, path string, v interface{}) (MetaData, error) {
fp, err := fsys.Open(path)
if err != nil {
return MetaData{}, err
}
defer fp.Close()
return NewDecoder(fp).Decode(v)
}

View File

@ -5,17 +5,25 @@ import (
"io" "io"
) )
// TextMarshaler is an alias for encoding.TextMarshaler.
//
// Deprecated: use encoding.TextMarshaler // Deprecated: use encoding.TextMarshaler
type TextMarshaler encoding.TextMarshaler type TextMarshaler encoding.TextMarshaler
// TextUnmarshaler is an alias for encoding.TextUnmarshaler.
//
// Deprecated: use encoding.TextUnmarshaler // Deprecated: use encoding.TextUnmarshaler
type TextUnmarshaler encoding.TextUnmarshaler type TextUnmarshaler encoding.TextUnmarshaler
// DecodeReader is an alias for NewDecoder(r).Decode(v).
//
// Deprecated: use NewDecoder(reader).Decode(&value).
func DecodeReader(r io.Reader, v any) (MetaData, error) { return NewDecoder(r).Decode(v) }
// PrimitiveDecode is an alias for MetaData.PrimitiveDecode().
//
// Deprecated: use MetaData.PrimitiveDecode. // Deprecated: use MetaData.PrimitiveDecode.
func PrimitiveDecode(primValue Primitive, v interface{}) error { func PrimitiveDecode(primValue Primitive, v any) error {
md := MetaData{decoded: make(map[string]struct{})} md := MetaData{decoded: make(map[string]struct{})}
return md.unify(primValue.undecoded, rvalue(v)) return md.unify(primValue.undecoded, rvalue(v))
} }
// Deprecated: use NewDecoder(reader).Decode(&value).
func DecodeReader(r io.Reader, v interface{}) (MetaData, error) { return NewDecoder(r).Decode(v) }

View File

@ -1,13 +1,8 @@
/* // Package toml implements decoding and encoding of TOML files.
Package toml implements decoding and encoding of TOML files. //
// This package supports TOML v1.0.0, as specified at https://toml.io
This package supports TOML v1.0.0, as listed on https://toml.io //
// The github.com/BurntSushi/toml/cmd/tomlv package implements a TOML validator,
There is also support for delaying decoding with the Primitive type, and // and can be used to verify if TOML document is valid. It can also be used to
querying the set of keys in a TOML document with the MetaData type. // print the type of each key.
The github.com/BurntSushi/toml/cmd/tomlv package implements a TOML validator,
and can be used to verify if TOML document is valid. It can also be used to
print the type of each key.
*/
package toml package toml

View File

@ -2,7 +2,9 @@ package toml
import ( import (
"bufio" "bufio"
"bytes"
"encoding" "encoding"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -63,18 +65,38 @@ var dblQuotedReplacer = strings.NewReplacer(
"\x7f", `\u007f`, "\x7f", `\u007f`,
) )
var (
marshalToml = reflect.TypeOf((*Marshaler)(nil)).Elem()
marshalText = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
)
// Marshaler is the interface implemented by types that can marshal themselves // Marshaler is the interface implemented by types that can marshal themselves
// into valid TOML. // into valid TOML.
type Marshaler interface { type Marshaler interface {
MarshalTOML() ([]byte, error) MarshalTOML() ([]byte, error)
} }
// Marshal returns a TOML representation of the Go value.
//
// See [Encoder] for a description of the encoding process.
func Marshal(v any) ([]byte, error) {
buff := new(bytes.Buffer)
if err := NewEncoder(buff).Encode(v); err != nil {
return nil, err
}
return buff.Bytes(), nil
}
// Encoder encodes a Go to a TOML document. // Encoder encodes a Go to a TOML document.
// //
// The mapping between Go values and TOML values should be precisely the same as // The mapping between Go values and TOML values should be precisely the same as
// for the Decode* functions. // for [Decode].
// //
// The toml.Marshaler and encoder.TextMarshaler interfaces are supported to // time.Time is encoded as a RFC 3339 string, and time.Duration as its string
// representation.
//
// The [Marshaler] and [encoding.TextMarshaler] interfaces are supported to
// encoding the value as custom TOML. // encoding the value as custom TOML.
// //
// If you want to write arbitrary binary data then you will need to use // If you want to write arbitrary binary data then you will need to use
@ -85,6 +107,17 @@ type Marshaler interface {
// //
// Go maps will be sorted alphabetically by key for deterministic output. // Go maps will be sorted alphabetically by key for deterministic output.
// //
// The toml struct tag can be used to provide the key name; if omitted the
// struct field name will be used. If the "omitempty" option is present the
// following value will be skipped:
//
// - arrays, slices, maps, and string with len of 0
// - struct with all zero values
// - bool false
//
// If omitzero is given all int and float types with a value of 0 will be
// skipped.
//
// Encoding Go values without a corresponding TOML representation will return an // Encoding Go values without a corresponding TOML representation will return an
// error. Examples of this includes maps with non-string keys, slices with nil // error. Examples of this includes maps with non-string keys, slices with nil
// elements, embedded non-struct types, and nested slices containing maps or // elements, embedded non-struct types, and nested slices containing maps or
@ -94,28 +127,24 @@ type Marshaler interface {
// NOTE: only exported keys are encoded due to the use of reflection. Unexported // NOTE: only exported keys are encoded due to the use of reflection. Unexported
// keys are silently discarded. // keys are silently discarded.
type Encoder struct { type Encoder struct {
// String to use for a single indentation level; default is two spaces. Indent string // string for a single indentation level; default is two spaces.
Indent string hasWritten bool // written any output to w yet?
w *bufio.Writer w *bufio.Writer
hasWritten bool // written any output to w yet?
} }
// NewEncoder create a new Encoder. // NewEncoder create a new Encoder.
func NewEncoder(w io.Writer) *Encoder { func NewEncoder(w io.Writer) *Encoder {
return &Encoder{ return &Encoder{w: bufio.NewWriter(w), Indent: " "}
w: bufio.NewWriter(w),
Indent: " ",
}
} }
// Encode writes a TOML representation of the Go value to the Encoder's writer. // Encode writes a TOML representation of the Go value to the [Encoder]'s writer.
// //
// An error is returned if the value given cannot be encoded to a valid TOML // An error is returned if the value given cannot be encoded to a valid TOML
// document. // document.
func (enc *Encoder) Encode(v interface{}) error { func (enc *Encoder) Encode(v any) error {
rv := eindirect(reflect.ValueOf(v)) rv := eindirect(reflect.ValueOf(v))
if err := enc.safeEncode(Key([]string{}), rv); err != nil { err := enc.safeEncode(Key([]string{}), rv)
if err != nil {
return err return err
} }
return enc.w.Flush() return enc.w.Flush()
@ -136,18 +165,15 @@ func (enc *Encoder) safeEncode(key Key, rv reflect.Value) (err error) {
} }
func (enc *Encoder) encode(key Key, rv reflect.Value) { func (enc *Encoder) encode(key Key, rv reflect.Value) {
// Special case: time needs to be in ISO8601 format. // If we can marshal the type to text, then we use that. This prevents the
// // encoder for handling these types as generic structs (or whatever the
// Special case: if we can marshal the type to text, then we used that. This // underlying type of a TextMarshaler is).
// prevents the encoder for handling these types as generic structs (or switch {
// whatever the underlying type of a TextMarshaler is). case isMarshaler(rv):
switch t := rv.Interface().(type) {
case time.Time, encoding.TextMarshaler, Marshaler:
enc.writeKeyValue(key, rv, false) enc.writeKeyValue(key, rv, false)
return return
// TODO: #76 would make this superfluous after implemented. case rv.Type() == primitiveType: // TODO: #76 would make this superfluous after implemented.
case Primitive: enc.encode(key, reflect.ValueOf(rv.Interface().(Primitive).undecoded))
enc.encode(key, reflect.ValueOf(t.undecoded))
return return
} }
@ -212,18 +238,44 @@ func (enc *Encoder) eElement(rv reflect.Value) {
if err != nil { if err != nil {
encPanic(err) encPanic(err)
} }
enc.writeQuoted(string(s)) if s == nil {
encPanic(errors.New("MarshalTOML returned nil and no error"))
}
enc.w.Write(s)
return return
case encoding.TextMarshaler: case encoding.TextMarshaler:
s, err := v.MarshalText() s, err := v.MarshalText()
if err != nil { if err != nil {
encPanic(err) encPanic(err)
} }
if s == nil {
encPanic(errors.New("MarshalText returned nil and no error"))
}
enc.writeQuoted(string(s)) enc.writeQuoted(string(s))
return return
case time.Duration:
enc.writeQuoted(v.String())
return
case json.Number:
n, _ := rv.Interface().(json.Number)
if n == "" { /// Useful zero value.
enc.w.WriteByte('0')
return
} else if v, err := n.Int64(); err == nil {
enc.eElement(reflect.ValueOf(v))
return
} else if v, err := n.Float64(); err == nil {
enc.eElement(reflect.ValueOf(v))
return
}
encPanic(fmt.Errorf("unable to convert %q to int64 or float64", n))
} }
switch rv.Kind() { switch rv.Kind() {
case reflect.Ptr:
enc.eElement(rv.Elem())
return
case reflect.String: case reflect.String:
enc.writeQuoted(rv.String()) enc.writeQuoted(rv.String())
case reflect.Bool: case reflect.Bool:
@ -235,18 +287,30 @@ func (enc *Encoder) eElement(rv reflect.Value) {
case reflect.Float32: case reflect.Float32:
f := rv.Float() f := rv.Float()
if math.IsNaN(f) { if math.IsNaN(f) {
if math.Signbit(f) {
enc.wf("-")
}
enc.wf("nan") enc.wf("nan")
} else if math.IsInf(f, 0) { } else if math.IsInf(f, 0) {
enc.wf("%cinf", map[bool]byte{true: '-', false: '+'}[math.Signbit(f)]) if math.Signbit(f) {
enc.wf("-")
}
enc.wf("inf")
} else { } else {
enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 32))) enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 32)))
} }
case reflect.Float64: case reflect.Float64:
f := rv.Float() f := rv.Float()
if math.IsNaN(f) { if math.IsNaN(f) {
if math.Signbit(f) {
enc.wf("-")
}
enc.wf("nan") enc.wf("nan")
} else if math.IsInf(f, 0) { } else if math.IsInf(f, 0) {
enc.wf("%cinf", map[bool]byte{true: '-', false: '+'}[math.Signbit(f)]) if math.Signbit(f) {
enc.wf("-")
}
enc.wf("inf")
} else { } else {
enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 64))) enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 64)))
} }
@ -259,7 +323,7 @@ func (enc *Encoder) eElement(rv reflect.Value) {
case reflect.Interface: case reflect.Interface:
enc.eElement(rv.Elem()) enc.eElement(rv.Elem())
default: default:
encPanic(fmt.Errorf("unexpected primitive type: %T", rv.Interface())) encPanic(fmt.Errorf("unexpected type: %s", fmtType(rv.Interface())))
} }
} }
@ -280,7 +344,7 @@ func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) {
length := rv.Len() length := rv.Len()
enc.wf("[") enc.wf("[")
for i := 0; i < length; i++ { for i := 0; i < length; i++ {
elem := rv.Index(i) elem := eindirect(rv.Index(i))
enc.eElement(elem) enc.eElement(elem)
if i != length-1 { if i != length-1 {
enc.wf(", ") enc.wf(", ")
@ -294,7 +358,7 @@ func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) {
encPanic(errNoKey) encPanic(errNoKey)
} }
for i := 0; i < rv.Len(); i++ { for i := 0; i < rv.Len(); i++ {
trv := rv.Index(i) trv := eindirect(rv.Index(i))
if isNil(trv) { if isNil(trv) {
continue continue
} }
@ -319,7 +383,7 @@ func (enc *Encoder) eTable(key Key, rv reflect.Value) {
} }
func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value, inline bool) { func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value, inline bool) {
switch rv := eindirect(rv); rv.Kind() { switch rv.Kind() {
case reflect.Map: case reflect.Map:
enc.eMap(key, rv, inline) enc.eMap(key, rv, inline)
case reflect.Struct: case reflect.Struct:
@ -341,7 +405,7 @@ func (enc *Encoder) eMap(key Key, rv reflect.Value, inline bool) {
var mapKeysDirect, mapKeysSub []string var mapKeysDirect, mapKeysSub []string
for _, mapKey := range rv.MapKeys() { for _, mapKey := range rv.MapKeys() {
k := mapKey.String() k := mapKey.String()
if typeIsTable(tomlTypeOfGo(rv.MapIndex(mapKey))) { if typeIsTable(tomlTypeOfGo(eindirect(rv.MapIndex(mapKey)))) {
mapKeysSub = append(mapKeysSub, k) mapKeysSub = append(mapKeysSub, k)
} else { } else {
mapKeysDirect = append(mapKeysDirect, k) mapKeysDirect = append(mapKeysDirect, k)
@ -351,7 +415,7 @@ func (enc *Encoder) eMap(key Key, rv reflect.Value, inline bool) {
var writeMapKeys = func(mapKeys []string, trailC bool) { var writeMapKeys = func(mapKeys []string, trailC bool) {
sort.Strings(mapKeys) sort.Strings(mapKeys)
for i, mapKey := range mapKeys { for i, mapKey := range mapKeys {
val := rv.MapIndex(reflect.ValueOf(mapKey)) val := eindirect(rv.MapIndex(reflect.ValueOf(mapKey)))
if isNil(val) { if isNil(val) {
continue continue
} }
@ -379,6 +443,13 @@ func (enc *Encoder) eMap(key Key, rv reflect.Value, inline bool) {
const is32Bit = (32 << (^uint(0) >> 63)) == 32 const is32Bit = (32 << (^uint(0) >> 63)) == 32
func pointerTo(t reflect.Type) reflect.Type {
if t.Kind() == reflect.Ptr {
return pointerTo(t.Elem())
}
return t
}
func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) { func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) {
// Write keys for fields directly under this key first, because if we write // Write keys for fields directly under this key first, because if we write
// a field that creates a new table then all keys under it will be in that // a field that creates a new table then all keys under it will be in that
@ -395,48 +466,42 @@ func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) {
addFields = func(rt reflect.Type, rv reflect.Value, start []int) { addFields = func(rt reflect.Type, rv reflect.Value, start []int) {
for i := 0; i < rt.NumField(); i++ { for i := 0; i < rt.NumField(); i++ {
f := rt.Field(i) f := rt.Field(i)
if f.PkgPath != "" && !f.Anonymous { /// Skip unexported fields. isEmbed := f.Anonymous && pointerTo(f.Type).Kind() == reflect.Struct
if f.PkgPath != "" && !isEmbed { /// Skip unexported fields.
continue
}
opts := getOptions(f.Tag)
if opts.skip {
continue continue
} }
frv := rv.Field(i) frv := eindirect(rv.Field(i))
if is32Bit {
// Copy so it works correct on 32bit archs; not clear why this
// is needed. See #314, and https://www.reddit.com/r/golang/comments/pnx8v4
// This also works fine on 64bit, but 32bit archs are somewhat
// rare and this is a wee bit faster.
copyStart := make([]int, len(start))
copy(copyStart, start)
start = copyStart
}
// Treat anonymous struct fields with tag names as though they are // Treat anonymous struct fields with tag names as though they are
// not anonymous, like encoding/json does. // not anonymous, like encoding/json does.
// //
// Non-struct anonymous fields use the normal encoding logic. // Non-struct anonymous fields use the normal encoding logic.
if f.Anonymous { if isEmbed {
t := f.Type if getOptions(f.Tag).name == "" && frv.Kind() == reflect.Struct {
switch t.Kind() { addFields(frv.Type(), frv, append(start, f.Index...))
case reflect.Struct: continue
if getOptions(f.Tag).name == "" {
addFields(t, frv, append(start, f.Index...))
continue
}
case reflect.Ptr:
if t.Elem().Kind() == reflect.Struct && getOptions(f.Tag).name == "" {
if !frv.IsNil() {
addFields(t.Elem(), frv.Elem(), append(start, f.Index...))
}
continue
}
} }
} }
if typeIsTable(tomlTypeOfGo(frv)) { if typeIsTable(tomlTypeOfGo(frv)) {
fieldsSub = append(fieldsSub, append(start, f.Index...)) fieldsSub = append(fieldsSub, append(start, f.Index...))
} else { } else {
// Copy so it works correct on 32bit archs; not clear why this fieldsDirect = append(fieldsDirect, append(start, f.Index...))
// is needed. See #314, and https://www.reddit.com/r/golang/comments/pnx8v4
// This also works fine on 64bit, but 32bit archs are somewhat
// rare and this is a wee bit faster.
if is32Bit {
copyStart := make([]int, len(start))
copy(copyStart, start)
fieldsDirect = append(fieldsDirect, append(copyStart, f.Index...))
} else {
fieldsDirect = append(fieldsDirect, append(start, f.Index...))
}
} }
} }
} }
@ -447,21 +512,25 @@ func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) {
fieldType := rt.FieldByIndex(fieldIndex) fieldType := rt.FieldByIndex(fieldIndex)
fieldVal := rv.FieldByIndex(fieldIndex) fieldVal := rv.FieldByIndex(fieldIndex)
if isNil(fieldVal) { /// Don't write anything for nil fields.
continue
}
opts := getOptions(fieldType.Tag) opts := getOptions(fieldType.Tag)
if opts.skip { if opts.skip {
continue continue
} }
if opts.omitempty && isEmpty(fieldVal) {
continue
}
fieldVal = eindirect(fieldVal)
if isNil(fieldVal) { /// Don't write anything for nil fields.
continue
}
keyName := fieldType.Name keyName := fieldType.Name
if opts.name != "" { if opts.name != "" {
keyName = opts.name keyName = opts.name
} }
if opts.omitempty && isEmpty(fieldVal) {
continue
}
if opts.omitzero && isZero(fieldVal) { if opts.omitzero && isZero(fieldVal) {
continue continue
} }
@ -498,6 +567,21 @@ func tomlTypeOfGo(rv reflect.Value) tomlType {
if isNil(rv) || !rv.IsValid() { if isNil(rv) || !rv.IsValid() {
return nil return nil
} }
if rv.Kind() == reflect.Struct {
if rv.Type() == timeType {
return tomlDatetime
}
if isMarshaler(rv) {
return tomlString
}
return tomlHash
}
if isMarshaler(rv) {
return tomlString
}
switch rv.Kind() { switch rv.Kind() {
case reflect.Bool: case reflect.Bool:
return tomlBool return tomlBool
@ -509,7 +593,7 @@ func tomlTypeOfGo(rv reflect.Value) tomlType {
case reflect.Float32, reflect.Float64: case reflect.Float32, reflect.Float64:
return tomlFloat return tomlFloat
case reflect.Array, reflect.Slice: case reflect.Array, reflect.Slice:
if typeEqual(tomlHash, tomlArrayType(rv)) { if isTableArray(rv) {
return tomlArrayHash return tomlArrayHash
} }
return tomlArray return tomlArray
@ -519,67 +603,35 @@ func tomlTypeOfGo(rv reflect.Value) tomlType {
return tomlString return tomlString
case reflect.Map: case reflect.Map:
return tomlHash return tomlHash
case reflect.Struct:
if _, ok := rv.Interface().(time.Time); ok {
return tomlDatetime
}
if isMarshaler(rv) {
return tomlString
}
return tomlHash
default: default:
if isMarshaler(rv) {
return tomlString
}
encPanic(errors.New("unsupported type: " + rv.Kind().String())) encPanic(errors.New("unsupported type: " + rv.Kind().String()))
panic("unreachable") panic("unreachable")
} }
} }
func isMarshaler(rv reflect.Value) bool { func isMarshaler(rv reflect.Value) bool {
switch rv.Interface().(type) { return rv.Type().Implements(marshalText) || rv.Type().Implements(marshalToml)
case encoding.TextMarshaler:
return true
case Marshaler:
return true
}
// Someone used a pointer receiver: we can make it work for pointer values.
if rv.CanAddr() {
if _, ok := rv.Addr().Interface().(encoding.TextMarshaler); ok {
return true
}
if _, ok := rv.Addr().Interface().(Marshaler); ok {
return true
}
}
return false
} }
// tomlArrayType returns the element type of a TOML array. The type returned // isTableArray reports if all entries in the array or slice are a table.
// may be nil if it cannot be determined (e.g., a nil slice or a zero length func isTableArray(arr reflect.Value) bool {
// slize). This function may also panic if it finds a type that cannot be if isNil(arr) || !arr.IsValid() || arr.Len() == 0 {
// expressed in TOML (such as nil elements, heterogeneous arrays or directly return false
// nested arrays of tables).
func tomlArrayType(rv reflect.Value) tomlType {
if isNil(rv) || !rv.IsValid() || rv.Len() == 0 {
return nil
} }
/// Don't allow nil. ret := true
rvlen := rv.Len() for i := 0; i < arr.Len(); i++ {
for i := 1; i < rvlen; i++ { tt := tomlTypeOfGo(eindirect(arr.Index(i)))
if tomlTypeOfGo(rv.Index(i)) == nil { // Don't allow nil.
if tt == nil {
encPanic(errArrayNilElement) encPanic(errArrayNilElement)
} }
}
firstType := tomlTypeOfGo(rv.Index(0)) if ret && !typeEqual(tomlHash, tt) {
if firstType == nil { ret = false
encPanic(errArrayNilElement) }
} }
return firstType return ret
} }
type tagOptions struct { type tagOptions struct {
@ -624,8 +676,26 @@ func isEmpty(rv reflect.Value) bool {
switch rv.Kind() { switch rv.Kind() {
case reflect.Array, reflect.Slice, reflect.Map, reflect.String: case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
return rv.Len() == 0 return rv.Len() == 0
case reflect.Struct:
if rv.Type().Comparable() {
return reflect.Zero(rv.Type()).Interface() == rv.Interface()
}
// Need to also check if all the fields are empty, otherwise something
// like this with uncomparable types will always return true:
//
// type a struct{ field b }
// type b struct{ s []string }
// s := a{field: b{s: []string{"AAA"}}}
for i := 0; i < rv.NumField(); i++ {
if !isEmpty(rv.Field(i)) {
return false
}
}
return true
case reflect.Bool: case reflect.Bool:
return !rv.Bool() return !rv.Bool()
case reflect.Ptr:
return rv.IsNil()
} }
return false return false
} }
@ -638,19 +708,21 @@ func (enc *Encoder) newline() {
// Write a key/value pair: // Write a key/value pair:
// //
// key = <any value> // key = <any value>
// //
// This is also used for "k = v" in inline tables; so something like this will // This is also used for "k = v" in inline tables; so something like this will
// be written in three calls: // be written in three calls:
// //
// ┌────────────────────┐ // ┌───────────────────┐
// │ ┌───┐ ┌─────┐│ // │ ┌───┐ ┌────┐│
// v v v v vv // v v v v vv
// key = {k = v, k2 = v2} // key = {k = 1, k2 = 2}
//
func (enc *Encoder) writeKeyValue(key Key, val reflect.Value, inline bool) { func (enc *Encoder) writeKeyValue(key Key, val reflect.Value, inline bool) {
/// Marshaler used on top-level document; call eElement() to just call
/// Marshal{TOML,Text}.
if len(key) == 0 { if len(key) == 0 {
encPanic(errNoKey) enc.eElement(val)
return
} }
enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1)) enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1))
enc.eElement(val) enc.eElement(val)
@ -659,7 +731,7 @@ func (enc *Encoder) writeKeyValue(key Key, val reflect.Value, inline bool) {
} }
} }
func (enc *Encoder) wf(format string, v ...interface{}) { func (enc *Encoder) wf(format string, v ...any) {
_, err := fmt.Fprintf(enc.w, format, v...) _, err := fmt.Fprintf(enc.w, format, v...)
if err != nil { if err != nil {
encPanic(err) encPanic(err)
@ -675,13 +747,25 @@ func encPanic(err error) {
panic(tomlEncodeError{err}) panic(tomlEncodeError{err})
} }
// Resolve any level of pointers to the actual value (e.g. **string → string).
func eindirect(v reflect.Value) reflect.Value { func eindirect(v reflect.Value) reflect.Value {
switch v.Kind() { if v.Kind() != reflect.Ptr && v.Kind() != reflect.Interface {
case reflect.Ptr, reflect.Interface: if isMarshaler(v) {
return eindirect(v.Elem()) return v
default: }
if v.CanAddr() { /// Special case for marshalers; see #358.
if pv := v.Addr(); isMarshaler(pv) {
return pv
}
}
return v return v
} }
if v.IsNil() {
return v
}
return eindirect(v.Elem())
} }
func isNil(rv reflect.Value) bool { func isNil(rv reflect.Value) bool {

View File

@ -5,57 +5,60 @@ import (
"strings" "strings"
) )
// ParseError is returned when there is an error parsing the TOML syntax. // ParseError is returned when there is an error parsing the TOML syntax such as
// // invalid syntax, duplicate keys, etc.
// For example invalid syntax, duplicate keys, etc.
// //
// In addition to the error message itself, you can also print detailed location // In addition to the error message itself, you can also print detailed location
// information with context by using ErrorWithLocation(): // information with context by using [ErrorWithPosition]:
// //
// toml: error: Key 'fruit' was already created and cannot be used as an array. // toml: error: Key 'fruit' was already created and cannot be used as an array.
// //
// At line 4, column 2-7: // At line 4, column 2-7:
// //
// 2 | fruit = [] // 2 | fruit = []
// 3 | // 3 |
// 4 | [[fruit]] # Not allowed // 4 | [[fruit]] # Not allowed
// ^^^^^ // ^^^^^
// //
// Furthermore, the ErrorWithUsage() can be used to print the above with some // [ErrorWithUsage] can be used to print the above with some more detailed usage
// more detailed usage guidance: // guidance:
// //
// toml: error: newlines not allowed within inline tables // toml: error: newlines not allowed within inline tables
// //
// At line 1, column 18: // At line 1, column 18:
// //
// 1 | x = [{ key = 42 # // 1 | x = [{ key = 42 #
// ^ // ^
// //
// Error help: // Error help:
// //
// Inline tables must always be on a single line: // Inline tables must always be on a single line:
// //
// table = {key = 42, second = 43} // table = {key = 42, second = 43}
// //
// It is invalid to split them over multiple lines like so: // It is invalid to split them over multiple lines like so:
// //
// # INVALID // # INVALID
// table = { // table = {
// key = 42, // key = 42,
// second = 43 // second = 43
// } // }
// //
// Use regular for this: // Use regular for this:
// //
// [table] // [table]
// key = 42 // key = 42
// second = 43 // second = 43
type ParseError struct { type ParseError struct {
Message string // Short technical message. Message string // Short technical message.
Usage string // Longer message with usage guidance; may be blank. Usage string // Longer message with usage guidance; may be blank.
Position Position // Position of the error Position Position // Position of the error
LastKey string // Last parsed key, may be blank. LastKey string // Last parsed key, may be blank.
Line int // Line the error occurred. Deprecated: use Position.
// Line the error occurred.
//
// Deprecated: use [Position].
Line int
err error err error
input string input string
@ -81,9 +84,9 @@ func (pe ParseError) Error() string {
pe.Position.Line, pe.LastKey, msg) pe.Position.Line, pe.LastKey, msg)
} }
// ErrorWithUsage() returns the error with detailed location context. // ErrorWithPosition returns the error with detailed location context.
// //
// See the documentation on ParseError. // See the documentation on [ParseError].
func (pe ParseError) ErrorWithPosition() string { func (pe ParseError) ErrorWithPosition() string {
if pe.input == "" { // Should never happen, but just in case. if pe.input == "" { // Should never happen, but just in case.
return pe.Error() return pe.Error()
@ -111,26 +114,39 @@ func (pe ParseError) ErrorWithPosition() string {
msg, pe.Position.Line, col, col+pe.Position.Len) msg, pe.Position.Line, col, col+pe.Position.Len)
} }
if pe.Position.Line > 2 { if pe.Position.Line > 2 {
fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-2, lines[pe.Position.Line-3]) fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-2, expandTab(lines[pe.Position.Line-3]))
} }
if pe.Position.Line > 1 { if pe.Position.Line > 1 {
fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-1, lines[pe.Position.Line-2]) fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-1, expandTab(lines[pe.Position.Line-2]))
} }
fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line, lines[pe.Position.Line-1])
fmt.Fprintf(b, "% 10s%s%s\n", "", strings.Repeat(" ", col), strings.Repeat("^", pe.Position.Len)) /// Expand tabs, so that the ^^^s are at the correct position, but leave
/// "column 10-13" intact. Adjusting this to the visual column would be
/// better, but we don't know the tabsize of the user in their editor, which
/// can be 8, 4, 2, or something else. We can't know. So leaving it as the
/// character index is probably the "most correct".
expanded := expandTab(lines[pe.Position.Line-1])
diff := len(expanded) - len(lines[pe.Position.Line-1])
fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line, expanded)
fmt.Fprintf(b, "% 10s%s%s\n", "", strings.Repeat(" ", col+diff), strings.Repeat("^", pe.Position.Len))
return b.String() return b.String()
} }
// ErrorWithUsage() returns the error with detailed location context and usage // ErrorWithUsage returns the error with detailed location context and usage
// guidance. // guidance.
// //
// See the documentation on ParseError. // See the documentation on [ParseError].
func (pe ParseError) ErrorWithUsage() string { func (pe ParseError) ErrorWithUsage() string {
m := pe.ErrorWithPosition() m := pe.ErrorWithPosition()
if u, ok := pe.err.(interface{ Usage() string }); ok && u.Usage() != "" { if u, ok := pe.err.(interface{ Usage() string }); ok && u.Usage() != "" {
return m + "Error help:\n\n " + lines := strings.Split(strings.TrimSpace(u.Usage()), "\n")
strings.ReplaceAll(strings.TrimSpace(u.Usage()), "\n", "\n ") + for i := range lines {
"\n" if lines[i] != "" {
lines[i] = " " + lines[i]
}
}
return m + "Error help:\n\n" + strings.Join(lines, "\n") + "\n"
} }
return m return m
} }
@ -152,14 +168,49 @@ func (pe ParseError) column(lines []string) int {
return col return col
} }
func expandTab(s string) string {
var (
b strings.Builder
l int
fill = func(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = ' '
}
return string(b)
}
)
b.Grow(len(s))
for _, r := range s {
switch r {
case '\t':
tw := 8 - l%8
b.WriteString(fill(tw))
l += tw
default:
b.WriteRune(r)
l += 1
}
}
return b.String()
}
type ( type (
errLexControl struct{ r rune } errLexControl struct{ r rune }
errLexEscape struct{ r rune } errLexEscape struct{ r rune }
errLexUTF8 struct{ b byte } errLexUTF8 struct{ b byte }
errLexInvalidNum struct{ v string } errParseDate struct{ v string }
errLexInvalidDate struct{ v string }
errLexInlineTableNL struct{} errLexInlineTableNL struct{}
errLexStringNL struct{} errLexStringNL struct{}
errParseRange struct {
i any // int or float
size string // "int64", "uint16", etc.
}
errUnsafeFloat struct {
i interface{} // float32 or float64
size string // "float32" or "float64"
}
errParseDuration struct{ d string }
) )
func (e errLexControl) Error() string { func (e errLexControl) Error() string {
@ -171,14 +222,20 @@ func (e errLexEscape) Error() string { return fmt.Sprintf(`invalid escape
func (e errLexEscape) Usage() string { return usageEscape } func (e errLexEscape) Usage() string { return usageEscape }
func (e errLexUTF8) Error() string { return fmt.Sprintf("invalid UTF-8 byte: 0x%02x", e.b) } func (e errLexUTF8) Error() string { return fmt.Sprintf("invalid UTF-8 byte: 0x%02x", e.b) }
func (e errLexUTF8) Usage() string { return "" } func (e errLexUTF8) Usage() string { return "" }
func (e errLexInvalidNum) Error() string { return fmt.Sprintf("invalid number: %q", e.v) } func (e errParseDate) Error() string { return fmt.Sprintf("invalid datetime: %q", e.v) }
func (e errLexInvalidNum) Usage() string { return "" } func (e errParseDate) Usage() string { return usageDate }
func (e errLexInvalidDate) Error() string { return fmt.Sprintf("invalid date: %q", e.v) }
func (e errLexInvalidDate) Usage() string { return "" }
func (e errLexInlineTableNL) Error() string { return "newlines not allowed within inline tables" } func (e errLexInlineTableNL) Error() string { return "newlines not allowed within inline tables" }
func (e errLexInlineTableNL) Usage() string { return usageInlineNewline } func (e errLexInlineTableNL) Usage() string { return usageInlineNewline }
func (e errLexStringNL) Error() string { return "strings cannot contain newlines" } func (e errLexStringNL) Error() string { return "strings cannot contain newlines" }
func (e errLexStringNL) Usage() string { return usageStringNewline } func (e errLexStringNL) Usage() string { return usageStringNewline }
func (e errParseRange) Error() string { return fmt.Sprintf("%v is out of range for %s", e.i, e.size) }
func (e errParseRange) Usage() string { return usageIntOverflow }
func (e errUnsafeFloat) Error() string {
return fmt.Sprintf("%v is out of the safe %s range", e.i, e.size)
}
func (e errUnsafeFloat) Usage() string { return usageUnsafeFloat }
func (e errParseDuration) Error() string { return fmt.Sprintf("invalid duration: %q", e.d) }
func (e errParseDuration) Usage() string { return usageDuration }
const usageEscape = ` const usageEscape = `
A '\' inside a "-delimited string is interpreted as an escape character. A '\' inside a "-delimited string is interpreted as an escape character.
@ -227,3 +284,73 @@ Instead use """ or ''' to split strings over multiple lines:
string = """Hello, string = """Hello,
world!""" world!"""
` `
const usageIntOverflow = `
This number is too large; this may be an error in the TOML, but it can also be a
bug in the program that uses too small of an integer.
The maximum and minimum values are:
size lowest highest
int8 -128 127
int16 -32,768 32,767
int32 -2,147,483,648 2,147,483,647
int64 -9.2 × 10¹ 9.2 × 10¹
uint8 0 255
uint16 0 65,535
uint32 0 4,294,967,295
uint64 0 1.8 × 10¹
int refers to int32 on 32-bit systems and int64 on 64-bit systems.
`
const usageUnsafeFloat = `
This number is outside of the "safe" range for floating point numbers; whole
(non-fractional) numbers outside the below range can not always be represented
accurately in a float, leading to some loss of accuracy.
Explicitly mark a number as a fractional unit by adding ".0", which will incur
some loss of accuracy; for example:
f = 2_000_000_000.0
Accuracy ranges:
float32 = 16,777,215
float64 = 9,007,199,254,740,991
`
const usageDuration = `
A duration must be as "number<unit>", without any spaces. Valid units are:
ns nanoseconds (billionth of a second)
us, µs microseconds (millionth of a second)
ms milliseconds (thousands of a second)
s seconds
m minutes
h hours
You can combine multiple units; for example "5m10s" for 5 minutes and 10
seconds.
`
const usageDate = `
A TOML datetime must be in one of the following formats:
2006-01-02T15:04:05Z07:00 Date and time, with timezone.
2006-01-02T15:04:05 Date and time, but without timezone.
2006-01-02 Date without a time or timezone.
15:04:05 Just a time, without any timezone.
Seconds may optionally have a fraction, up to nanosecond precision:
15:04:05.123
15:04:05.856018510
`
// TOML 1.1:
// The seconds part in times is optional, and may be omitted:
// 2006-01-02T15:04Z07:00
// 2006-01-02T15:04
// 15:04

View File

@ -17,6 +17,7 @@ const (
itemEOF itemEOF
itemText itemText
itemString itemString
itemStringEsc
itemRawString itemRawString
itemMultilineString itemMultilineString
itemRawMultilineString itemRawMultilineString
@ -46,12 +47,14 @@ func (p Position) String() string {
} }
type lexer struct { type lexer struct {
input string input string
start int start int
pos int pos int
line int line int
state stateFn state stateFn
items chan item items chan item
tomlNext bool
esc bool
// Allow for backing up up to 4 runes. This is necessary because TOML // Allow for backing up up to 4 runes. This is necessary because TOML
// contains 3-rune tokens (""" and '''). // contains 3-rune tokens (""" and ''').
@ -82,18 +85,19 @@ func (lx *lexer) nextItem() item {
return item return item
default: default:
lx.state = lx.state(lx) lx.state = lx.state(lx)
//fmt.Printf(" STATE %-24s current: %-10q stack: %s\n", lx.state, lx.current(), lx.stack) //fmt.Printf(" STATE %-24s current: %-10s stack: %s\n", lx.state, lx.current(), lx.stack)
} }
} }
} }
func lex(input string) *lexer { func lex(input string, tomlNext bool) *lexer {
lx := &lexer{ lx := &lexer{
input: input, input: input,
state: lexTop, state: lexTop,
items: make(chan item, 10), items: make(chan item, 10),
stack: make([]stateFn, 0, 10), stack: make([]stateFn, 0, 10),
line: 1, line: 1,
tomlNext: tomlNext,
} }
return lx return lx
} }
@ -128,6 +132,11 @@ func (lx lexer) getPos() Position {
} }
func (lx *lexer) emit(typ itemType) { func (lx *lexer) emit(typ itemType) {
// Needed for multiline strings ending with an incomplete UTF-8 sequence.
if lx.start > lx.pos {
lx.error(errLexUTF8{lx.input[lx.pos]})
return
}
lx.items <- item{typ: typ, pos: lx.getPos(), val: lx.current()} lx.items <- item{typ: typ, pos: lx.getPos(), val: lx.current()}
lx.start = lx.pos lx.start = lx.pos
} }
@ -157,7 +166,7 @@ func (lx *lexer) next() (r rune) {
} }
r, w := utf8.DecodeRuneInString(lx.input[lx.pos:]) r, w := utf8.DecodeRuneInString(lx.input[lx.pos:])
if r == utf8.RuneError { if r == utf8.RuneError && w == 1 {
lx.error(errLexUTF8{lx.input[lx.pos]}) lx.error(errLexUTF8{lx.input[lx.pos]})
return utf8.RuneError return utf8.RuneError
} }
@ -263,7 +272,7 @@ func (lx *lexer) errorPos(start, length int, err error) stateFn {
} }
// errorf is like error, and creates a new error. // errorf is like error, and creates a new error.
func (lx *lexer) errorf(format string, values ...interface{}) stateFn { func (lx *lexer) errorf(format string, values ...any) stateFn {
if lx.atEOF { if lx.atEOF {
pos := lx.getPos() pos := lx.getPos()
pos.Line-- pos.Line--
@ -326,9 +335,7 @@ func lexTopEnd(lx *lexer) stateFn {
lx.emit(itemEOF) lx.emit(itemEOF)
return nil return nil
} }
return lx.errorf( return lx.errorf("expected a top-level item to end with a newline, comment, or EOF, but got %q instead", r)
"expected a top-level item to end with a newline, comment, or EOF, but got %q instead",
r)
} }
// lexTable lexes the beginning of a table. Namely, it makes sure that // lexTable lexes the beginning of a table. Namely, it makes sure that
@ -403,7 +410,7 @@ func lexTableNameEnd(lx *lexer) stateFn {
// Lexes only one part, e.g. only 'a' inside 'a.b'. // Lexes only one part, e.g. only 'a' inside 'a.b'.
func lexBareName(lx *lexer) stateFn { func lexBareName(lx *lexer) stateFn {
r := lx.next() r := lx.next()
if isBareKeyChar(r) { if isBareKeyChar(r, lx.tomlNext) {
return lexBareName return lexBareName
} }
lx.backup() lx.backup()
@ -613,6 +620,9 @@ func lexInlineTableValue(lx *lexer) stateFn {
case isWhitespace(r): case isWhitespace(r):
return lexSkip(lx, lexInlineTableValue) return lexSkip(lx, lexInlineTableValue)
case isNL(r): case isNL(r):
if lx.tomlNext {
return lexSkip(lx, lexInlineTableValue)
}
return lx.errorPrevLine(errLexInlineTableNL{}) return lx.errorPrevLine(errLexInlineTableNL{})
case r == '#': case r == '#':
lx.push(lexInlineTableValue) lx.push(lexInlineTableValue)
@ -635,6 +645,9 @@ func lexInlineTableValueEnd(lx *lexer) stateFn {
case isWhitespace(r): case isWhitespace(r):
return lexSkip(lx, lexInlineTableValueEnd) return lexSkip(lx, lexInlineTableValueEnd)
case isNL(r): case isNL(r):
if lx.tomlNext {
return lexSkip(lx, lexInlineTableValueEnd)
}
return lx.errorPrevLine(errLexInlineTableNL{}) return lx.errorPrevLine(errLexInlineTableNL{})
case r == '#': case r == '#':
lx.push(lexInlineTableValueEnd) lx.push(lexInlineTableValueEnd)
@ -643,6 +656,9 @@ func lexInlineTableValueEnd(lx *lexer) stateFn {
lx.ignore() lx.ignore()
lx.skip(isWhitespace) lx.skip(isWhitespace)
if lx.peek() == '}' { if lx.peek() == '}' {
if lx.tomlNext {
return lexInlineTableValueEnd
}
return lx.errorf("trailing comma not allowed in inline tables") return lx.errorf("trailing comma not allowed in inline tables")
} }
return lexInlineTableValue return lexInlineTableValue
@ -682,7 +698,12 @@ func lexString(lx *lexer) stateFn {
return lexStringEscape return lexStringEscape
case r == '"': case r == '"':
lx.backup() lx.backup()
lx.emit(itemString) if lx.esc {
lx.esc = false
lx.emit(itemStringEsc)
} else {
lx.emit(itemString)
}
lx.next() lx.next()
lx.ignore() lx.ignore()
return lx.pop() return lx.pop()
@ -711,7 +732,17 @@ func lexMultilineString(lx *lexer) stateFn {
if lx.peek() == '"' { if lx.peek() == '"' {
/// Check if we already lexed 5 's; if so we have 6 now, and /// Check if we already lexed 5 's; if so we have 6 now, and
/// that's just too many man! /// that's just too many man!
if strings.HasSuffix(lx.current(), `"""""`) { ///
/// Second check is for the edge case:
///
/// two quotes allowed.
/// vv
/// """lol \""""""
/// ^^ ^^^---- closing three
/// escaped
///
/// But ugly, but it works
if strings.HasSuffix(lx.current(), `"""""`) && !strings.HasSuffix(lx.current(), `\"""""`) {
return lx.errorf(`unexpected '""""""'`) return lx.errorf(`unexpected '""""""'`)
} }
lx.backup() lx.backup()
@ -722,6 +753,7 @@ func lexMultilineString(lx *lexer) stateFn {
lx.backup() /// backup: don't include the """ in the item. lx.backup() /// backup: don't include the """ in the item.
lx.backup() lx.backup()
lx.backup() lx.backup()
lx.esc = false
lx.emit(itemMultilineString) lx.emit(itemMultilineString)
lx.next() /// Read over ''' again and discard it. lx.next() /// Read over ''' again and discard it.
lx.next() lx.next()
@ -755,8 +787,8 @@ func lexRawString(lx *lexer) stateFn {
} }
} }
// lexMultilineRawString consumes a raw string. Nothing can be escaped in such // lexMultilineRawString consumes a raw string. Nothing can be escaped in such a
// a string. It assumes that the beginning "'''" has already been consumed and // string. It assumes that the beginning triple-' has already been consumed and
// ignored. // ignored.
func lexMultilineRawString(lx *lexer) stateFn { func lexMultilineRawString(lx *lexer) stateFn {
r := lx.next() r := lx.next()
@ -802,8 +834,7 @@ func lexMultilineRawString(lx *lexer) stateFn {
// lexMultilineStringEscape consumes an escaped character. It assumes that the // lexMultilineStringEscape consumes an escaped character. It assumes that the
// preceding '\\' has already been consumed. // preceding '\\' has already been consumed.
func lexMultilineStringEscape(lx *lexer) stateFn { func lexMultilineStringEscape(lx *lexer) stateFn {
// Handle the special case first: if isNL(lx.next()) { /// \ escaping newline.
if isNL(lx.next()) {
return lexMultilineString return lexMultilineString
} }
lx.backup() lx.backup()
@ -812,8 +843,14 @@ func lexMultilineStringEscape(lx *lexer) stateFn {
} }
func lexStringEscape(lx *lexer) stateFn { func lexStringEscape(lx *lexer) stateFn {
lx.esc = true
r := lx.next() r := lx.next()
switch r { switch r {
case 'e':
if !lx.tomlNext {
return lx.error(errLexEscape{r})
}
fallthrough
case 'b': case 'b':
fallthrough fallthrough
case 't': case 't':
@ -832,6 +869,11 @@ func lexStringEscape(lx *lexer) stateFn {
fallthrough fallthrough
case '\\': case '\\':
return lx.pop() return lx.pop()
case 'x':
if !lx.tomlNext {
return lx.error(errLexEscape{r})
}
return lexHexEscape
case 'u': case 'u':
return lexShortUnicodeEscape return lexShortUnicodeEscape
case 'U': case 'U':
@ -840,14 +882,23 @@ func lexStringEscape(lx *lexer) stateFn {
return lx.error(errLexEscape{r}) return lx.error(errLexEscape{r})
} }
func lexHexEscape(lx *lexer) stateFn {
var r rune
for i := 0; i < 2; i++ {
r = lx.next()
if !isHex(r) {
return lx.errorf(`expected two hexadecimal digits after '\x', but got %q instead`, lx.current())
}
}
return lx.pop()
}
func lexShortUnicodeEscape(lx *lexer) stateFn { func lexShortUnicodeEscape(lx *lexer) stateFn {
var r rune var r rune
for i := 0; i < 4; i++ { for i := 0; i < 4; i++ {
r = lx.next() r = lx.next()
if !isHexadecimal(r) { if !isHex(r) {
return lx.errorf( return lx.errorf(`expected four hexadecimal digits after '\u', but got %q instead`, lx.current())
`expected four hexadecimal digits after '\u', but got %q instead`,
lx.current())
} }
} }
return lx.pop() return lx.pop()
@ -857,10 +908,8 @@ func lexLongUnicodeEscape(lx *lexer) stateFn {
var r rune var r rune
for i := 0; i < 8; i++ { for i := 0; i < 8; i++ {
r = lx.next() r = lx.next()
if !isHexadecimal(r) { if !isHex(r) {
return lx.errorf( return lx.errorf(`expected eight hexadecimal digits after '\U', but got %q instead`, lx.current())
`expected eight hexadecimal digits after '\U', but got %q instead`,
lx.current())
} }
} }
return lx.pop() return lx.pop()
@ -927,7 +976,7 @@ func lexDatetime(lx *lexer) stateFn {
// lexHexInteger consumes a hexadecimal integer after seeing the '0x' prefix. // lexHexInteger consumes a hexadecimal integer after seeing the '0x' prefix.
func lexHexInteger(lx *lexer) stateFn { func lexHexInteger(lx *lexer) stateFn {
r := lx.next() r := lx.next()
if isHexadecimal(r) { if isHex(r) {
return lexHexInteger return lexHexInteger
} }
switch r { switch r {
@ -1061,7 +1110,7 @@ func lexBaseNumberOrDate(lx *lexer) stateFn {
return lexOctalInteger return lexOctalInteger
case 'x': case 'x':
r = lx.peek() r = lx.peek()
if !isHexadecimal(r) { if !isHex(r) {
lx.errorf("not a hexidecimal number: '%s%c'", lx.current(), r) lx.errorf("not a hexidecimal number: '%s%c'", lx.current(), r)
} }
return lexHexInteger return lexHexInteger
@ -1159,7 +1208,7 @@ func (itype itemType) String() string {
return "EOF" return "EOF"
case itemText: case itemText:
return "Text" return "Text"
case itemString, itemRawString, itemMultilineString, itemRawMultilineString: case itemString, itemStringEsc, itemRawString, itemMultilineString, itemRawMultilineString:
return "String" return "String"
case itemBool: case itemBool:
return "Bool" return "Bool"
@ -1192,7 +1241,7 @@ func (itype itemType) String() string {
} }
func (item item) String() string { func (item item) String() string {
return fmt.Sprintf("(%s, %s)", item.typ.String(), item.val) return fmt.Sprintf("(%s, %s)", item.typ, item.val)
} }
func isWhitespace(r rune) bool { return r == '\t' || r == ' ' } func isWhitespace(r rune) bool { return r == '\t' || r == ' ' }
@ -1208,10 +1257,23 @@ func isControl(r rune) bool { // Control characters except \t, \r, \n
func isDigit(r rune) bool { return r >= '0' && r <= '9' } func isDigit(r rune) bool { return r >= '0' && r <= '9' }
func isBinary(r rune) bool { return r == '0' || r == '1' } func isBinary(r rune) bool { return r == '0' || r == '1' }
func isOctal(r rune) bool { return r >= '0' && r <= '7' } func isOctal(r rune) bool { return r >= '0' && r <= '7' }
func isHexadecimal(r rune) bool { func isHex(r rune) bool { return (r >= '0' && r <= '9') || (r|0x20 >= 'a' && r|0x20 <= 'f') }
return (r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F') func isBareKeyChar(r rune, tomlNext bool) bool {
} if tomlNext {
func isBareKeyChar(r rune) bool { return (r >= 'A' && r <= 'Z') ||
(r >= 'a' && r <= 'z') ||
(r >= '0' && r <= '9') ||
r == '_' || r == '-' ||
r == 0xb2 || r == 0xb3 || r == 0xb9 || (r >= 0xbc && r <= 0xbe) ||
(r >= 0xc0 && r <= 0xd6) || (r >= 0xd8 && r <= 0xf6) || (r >= 0xf8 && r <= 0x037d) ||
(r >= 0x037f && r <= 0x1fff) ||
(r >= 0x200c && r <= 0x200d) || (r >= 0x203f && r <= 0x2040) ||
(r >= 0x2070 && r <= 0x218f) || (r >= 0x2460 && r <= 0x24ff) ||
(r >= 0x2c00 && r <= 0x2fef) || (r >= 0x3001 && r <= 0xd7ff) ||
(r >= 0xf900 && r <= 0xfdcf) || (r >= 0xfdf0 && r <= 0xfffd) ||
(r >= 0x10000 && r <= 0xeffff)
}
return (r >= 'A' && r <= 'Z') || return (r >= 'A' && r <= 'Z') ||
(r >= 'a' && r <= 'z') || (r >= 'a' && r <= 'z') ||
(r >= '0' && r <= '9') || (r >= '0' && r <= '9') ||

View File

@ -12,10 +12,11 @@ import (
type MetaData struct { type MetaData struct {
context Key // Used only during decoding. context Key // Used only during decoding.
mapping map[string]interface{} keyInfo map[string]keyInfo
types map[string]tomlType mapping map[string]any
keys []Key keys []Key
decoded map[string]struct{} decoded map[string]struct{}
data []byte // Input file; for errors.
} }
// IsDefined reports if the key exists in the TOML data. // IsDefined reports if the key exists in the TOML data.
@ -30,12 +31,12 @@ func (md *MetaData) IsDefined(key ...string) bool {
} }
var ( var (
hash map[string]interface{} hash map[string]any
ok bool ok bool
hashOrVal interface{} = md.mapping hashOrVal any = md.mapping
) )
for _, k := range key { for _, k := range key {
if hash, ok = hashOrVal.(map[string]interface{}); !ok { if hash, ok = hashOrVal.(map[string]any); !ok {
return false return false
} }
if hashOrVal, ok = hash[k]; !ok { if hashOrVal, ok = hash[k]; !ok {
@ -50,8 +51,8 @@ func (md *MetaData) IsDefined(key ...string) bool {
// Type will return the empty string if given an empty key or a key that does // Type will return the empty string if given an empty key or a key that does
// not exist. Keys are case sensitive. // not exist. Keys are case sensitive.
func (md *MetaData) Type(key ...string) string { func (md *MetaData) Type(key ...string) string {
if typ, ok := md.types[Key(key).String()]; ok { if ki, ok := md.keyInfo[Key(key).String()]; ok {
return typ.typeString() return ki.tomlType.typeString()
} }
return "" return ""
} }
@ -70,7 +71,7 @@ func (md *MetaData) Keys() []Key {
// Undecoded returns all keys that have not been decoded in the order in which // Undecoded returns all keys that have not been decoded in the order in which
// they appear in the original TOML document. // they appear in the original TOML document.
// //
// This includes keys that haven't been decoded because of a Primitive value. // This includes keys that haven't been decoded because of a [Primitive] value.
// Once the Primitive value is decoded, the keys will be considered decoded. // Once the Primitive value is decoded, the keys will be considered decoded.
// //
// Also note that decoding into an empty interface will result in no decoding, // Also note that decoding into an empty interface will result in no decoding,
@ -88,33 +89,60 @@ func (md *MetaData) Undecoded() []Key {
return undecoded return undecoded
} }
// Key represents any TOML key, including key groups. Use (MetaData).Keys to get // Key represents any TOML key, including key groups. Use [MetaData.Keys] to get
// values of this type. // values of this type.
type Key []string type Key []string
func (k Key) String() string { func (k Key) String() string {
ss := make([]string, len(k)) // This is called quite often, so it's a bit funky to make it faster.
for i := range k { var b strings.Builder
ss[i] = k.maybeQuoted(i) b.Grow(len(k) * 25)
outer:
for i, kk := range k {
if i > 0 {
b.WriteByte('.')
}
if kk == "" {
b.WriteString(`""`)
} else {
for _, r := range kk {
// "Inline" isBareKeyChar
if !((r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' || r == '-') {
b.WriteByte('"')
b.WriteString(dblQuotedReplacer.Replace(kk))
b.WriteByte('"')
continue outer
}
}
b.WriteString(kk)
}
} }
return strings.Join(ss, ".") return b.String()
} }
func (k Key) maybeQuoted(i int) string { func (k Key) maybeQuoted(i int) string {
if k[i] == "" { if k[i] == "" {
return `""` return `""`
} }
for _, c := range k[i] { for _, r := range k[i] {
if !isBareKeyChar(c) { if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' || r == '-' {
return `"` + dblQuotedReplacer.Replace(k[i]) + `"` continue
} }
return `"` + dblQuotedReplacer.Replace(k[i]) + `"`
} }
return k[i] return k[i]
} }
// Like append(), but only increase the cap by 1.
func (k Key) add(piece string) Key { func (k Key) add(piece string) Key {
if cap(k) > len(k) {
return append(k, piece)
}
newKey := make(Key, len(k)+1) newKey := make(Key, len(k)+1)
copy(newKey, k) copy(newKey, k)
newKey[len(k)] = piece newKey[len(k)] = piece
return newKey return newKey
} }
func (k Key) parent() Key { return k[:len(k)-1] } // all except the last piece.
func (k Key) last() string { return k[len(k)-1] } // last piece of this key.

View File

@ -2,6 +2,8 @@ package toml
import ( import (
"fmt" "fmt"
"math"
"os"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -15,14 +17,23 @@ type parser struct {
context Key // Full key for the current hash in scope. context Key // Full key for the current hash in scope.
currentKey string // Base key name for everything except hashes. currentKey string // Base key name for everything except hashes.
pos Position // Current position in the TOML file. pos Position // Current position in the TOML file.
tomlNext bool
ordered []Key // List of keys in the order that they appear in the TOML data. ordered []Key // List of keys in the order that they appear in the TOML data.
mapping map[string]interface{} // Map keyname → key value.
types map[string]tomlType // Map keyname → TOML type. keyInfo map[string]keyInfo // Map keyname → info about the TOML key.
implicits map[string]struct{} // Record implicit keys (e.g. "key.group.names"). mapping map[string]any // Map keyname → key value.
implicits map[string]struct{} // Record implicit keys (e.g. "key.group.names").
}
type keyInfo struct {
pos Position
tomlType tomlType
} }
func parse(data string) (p *parser, err error) { func parse(data string) (p *parser, err error) {
_, tomlNext := os.LookupEnv("BURNTSUSHI_TOML_110")
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
if pErr, ok := r.(ParseError); ok { if pErr, ok := r.(ParseError); ok {
@ -35,9 +46,13 @@ func parse(data string) (p *parser, err error) {
}() }()
// Read over BOM; do this here as the lexer calls utf8.DecodeRuneInString() // Read over BOM; do this here as the lexer calls utf8.DecodeRuneInString()
// which mangles stuff. // which mangles stuff. UTF-16 BOM isn't strictly valid, but some tools add
if strings.HasPrefix(data, "\xff\xfe") || strings.HasPrefix(data, "\xfe\xff") { // it anyway.
if strings.HasPrefix(data, "\xff\xfe") || strings.HasPrefix(data, "\xfe\xff") { // UTF-16
data = data[2:] data = data[2:]
//lint:ignore S1017 https://github.com/dominikh/go-tools/issues/1447
} else if strings.HasPrefix(data, "\xef\xbb\xbf") { // UTF-8
data = data[3:]
} }
// Examine first few bytes for NULL bytes; this probably means it's a UTF-16 // Examine first few bytes for NULL bytes; this probably means it's a UTF-16
@ -57,11 +72,12 @@ func parse(data string) (p *parser, err error) {
} }
p = &parser{ p = &parser{
mapping: make(map[string]interface{}), keyInfo: make(map[string]keyInfo),
types: make(map[string]tomlType), mapping: make(map[string]any),
lx: lex(data), lx: lex(data, tomlNext),
ordered: make([]Key, 0), ordered: make([]Key, 0),
implicits: make(map[string]struct{}), implicits: make(map[string]struct{}),
tomlNext: tomlNext,
} }
for { for {
item := p.next() item := p.next()
@ -74,7 +90,16 @@ func parse(data string) (p *parser, err error) {
return p, nil return p, nil
} }
func (p *parser) panicItemf(it item, format string, v ...interface{}) { func (p *parser) panicErr(it item, err error) {
panic(ParseError{
err: err,
Position: it.pos,
Line: it.pos.Len,
LastKey: p.current(),
})
}
func (p *parser) panicItemf(it item, format string, v ...any) {
panic(ParseError{ panic(ParseError{
Message: fmt.Sprintf(format, v...), Message: fmt.Sprintf(format, v...),
Position: it.pos, Position: it.pos,
@ -83,7 +108,7 @@ func (p *parser) panicItemf(it item, format string, v ...interface{}) {
}) })
} }
func (p *parser) panicf(format string, v ...interface{}) { func (p *parser) panicf(format string, v ...any) {
panic(ParseError{ panic(ParseError{
Message: fmt.Sprintf(format, v...), Message: fmt.Sprintf(format, v...),
Position: p.pos, Position: p.pos,
@ -94,7 +119,7 @@ func (p *parser) panicf(format string, v ...interface{}) {
func (p *parser) next() item { func (p *parser) next() item {
it := p.lx.nextItem() it := p.lx.nextItem()
//fmt.Printf("ITEM %-18s line %-3d │ %q\n", it.typ, it.line, it.val) //fmt.Printf("ITEM %-18s line %-3d │ %q\n", it.typ, it.pos.Line, it.val)
if it.typ == itemError { if it.typ == itemError {
if it.err != nil { if it.err != nil {
panic(ParseError{ panic(ParseError{
@ -116,7 +141,7 @@ func (p *parser) nextPos() item {
return it return it
} }
func (p *parser) bug(format string, v ...interface{}) { func (p *parser) bug(format string, v ...any) {
panic(fmt.Sprintf("BUG: "+format+"\n\n", v...)) panic(fmt.Sprintf("BUG: "+format+"\n\n", v...))
} }
@ -146,7 +171,7 @@ func (p *parser) topLevel(item item) {
p.assertEqual(itemTableEnd, name.typ) p.assertEqual(itemTableEnd, name.typ)
p.addContext(key, false) p.addContext(key, false)
p.setType("", tomlHash) p.setType("", tomlHash, item.pos)
p.ordered = append(p.ordered, key) p.ordered = append(p.ordered, key)
case itemArrayTableStart: // [[ .. ]] case itemArrayTableStart: // [[ .. ]]
name := p.nextPos() name := p.nextPos()
@ -158,7 +183,7 @@ func (p *parser) topLevel(item item) {
p.assertEqual(itemArrayTableEnd, name.typ) p.assertEqual(itemArrayTableEnd, name.typ)
p.addContext(key, true) p.addContext(key, true)
p.setType("", tomlArrayHash) p.setType("", tomlArrayHash, item.pos)
p.ordered = append(p.ordered, key) p.ordered = append(p.ordered, key)
case itemKeyStart: // key = .. case itemKeyStart: // key = ..
outerContext := p.context outerContext := p.context
@ -171,19 +196,21 @@ func (p *parser) topLevel(item item) {
p.assertEqual(itemKeyEnd, k.typ) p.assertEqual(itemKeyEnd, k.typ)
/// The current key is the last part. /// The current key is the last part.
p.currentKey = key[len(key)-1] p.currentKey = key.last()
/// All the other parts (if any) are the context; need to set each part /// All the other parts (if any) are the context; need to set each part
/// as implicit. /// as implicit.
context := key[:len(key)-1] context := key.parent()
for i := range context { for i := range context {
p.addImplicitContext(append(p.context, context[i:i+1]...)) p.addImplicitContext(append(p.context, context[i:i+1]...))
} }
p.ordered = append(p.ordered, p.context.add(p.currentKey))
/// Set value. /// Set value.
val, typ := p.value(p.next(), false) vItem := p.next()
p.set(p.currentKey, val, typ) val, typ := p.value(vItem, false)
p.ordered = append(p.ordered, p.context.add(p.currentKey)) p.setValue(p.currentKey, val)
p.setType(p.currentKey, typ, vItem.pos)
/// Remove the context we added (preserving any context from [tbl] lines). /// Remove the context we added (preserving any context from [tbl] lines).
p.context = outerContext p.context = outerContext
@ -198,7 +225,7 @@ func (p *parser) keyString(it item) string {
switch it.typ { switch it.typ {
case itemText: case itemText:
return it.val return it.val
case itemString, itemMultilineString, case itemString, itemStringEsc, itemMultilineString,
itemRawString, itemRawMultilineString: itemRawString, itemRawMultilineString:
s, _ := p.value(it, false) s, _ := p.value(it, false)
return s.(string) return s.(string)
@ -215,12 +242,14 @@ var datetimeRepl = strings.NewReplacer(
// value translates an expected value from the lexer into a Go value wrapped // value translates an expected value from the lexer into a Go value wrapped
// as an empty interface. // as an empty interface.
func (p *parser) value(it item, parentIsArray bool) (interface{}, tomlType) { func (p *parser) value(it item, parentIsArray bool) (any, tomlType) {
switch it.typ { switch it.typ {
case itemString: case itemString:
return it.val, p.typeOfPrimitive(it)
case itemStringEsc:
return p.replaceEscapes(it, it.val), p.typeOfPrimitive(it) return p.replaceEscapes(it, it.val), p.typeOfPrimitive(it)
case itemMultilineString: case itemMultilineString:
return p.replaceEscapes(it, stripFirstNewline(stripEscapedNewlines(it.val))), p.typeOfPrimitive(it) return p.replaceEscapes(it, p.stripEscapedNewlines(stripFirstNewline(it.val))), p.typeOfPrimitive(it)
case itemRawString: case itemRawString:
return it.val, p.typeOfPrimitive(it) return it.val, p.typeOfPrimitive(it)
case itemRawMultilineString: case itemRawMultilineString:
@ -250,7 +279,7 @@ func (p *parser) value(it item, parentIsArray bool) (interface{}, tomlType) {
panic("unreachable") panic("unreachable")
} }
func (p *parser) valueInteger(it item) (interface{}, tomlType) { func (p *parser) valueInteger(it item) (any, tomlType) {
if !numUnderscoresOK(it.val) { if !numUnderscoresOK(it.val) {
p.panicItemf(it, "Invalid integer %q: underscores must be surrounded by digits", it.val) p.panicItemf(it, "Invalid integer %q: underscores must be surrounded by digits", it.val)
} }
@ -266,7 +295,7 @@ func (p *parser) valueInteger(it item) (interface{}, tomlType) {
// So mark the former as a bug but the latter as a legitimate user // So mark the former as a bug but the latter as a legitimate user
// error. // error.
if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange { if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange {
p.panicItemf(it, "Integer '%s' is out of the range of 64-bit signed integers.", it.val) p.panicErr(it, errParseRange{i: it.val, size: "int64"})
} else { } else {
p.bug("Expected integer value, but got '%s'.", it.val) p.bug("Expected integer value, but got '%s'.", it.val)
} }
@ -274,7 +303,7 @@ func (p *parser) valueInteger(it item) (interface{}, tomlType) {
return num, p.typeOfPrimitive(it) return num, p.typeOfPrimitive(it)
} }
func (p *parser) valueFloat(it item) (interface{}, tomlType) { func (p *parser) valueFloat(it item) (any, tomlType) {
parts := strings.FieldsFunc(it.val, func(r rune) bool { parts := strings.FieldsFunc(it.val, func(r rune) bool {
switch r { switch r {
case '.', 'e', 'E': case '.', 'e', 'E':
@ -298,31 +327,42 @@ func (p *parser) valueFloat(it item) (interface{}, tomlType) {
p.panicItemf(it, "Invalid float %q: '.' must be followed by one or more digits", it.val) p.panicItemf(it, "Invalid float %q: '.' must be followed by one or more digits", it.val)
} }
val := strings.Replace(it.val, "_", "", -1) val := strings.Replace(it.val, "_", "", -1)
if val == "+nan" || val == "-nan" { // Go doesn't support this, but TOML spec does. signbit := false
if val == "+nan" || val == "-nan" {
signbit = val == "-nan"
val = "nan" val = "nan"
} }
num, err := strconv.ParseFloat(val, 64) num, err := strconv.ParseFloat(val, 64)
if err != nil { if err != nil {
if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange { if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange {
p.panicItemf(it, "Float '%s' is out of the range of 64-bit IEEE-754 floating-point numbers.", it.val) p.panicErr(it, errParseRange{i: it.val, size: "float64"})
} else { } else {
p.panicItemf(it, "Invalid float value: %q", it.val) p.panicItemf(it, "Invalid float value: %q", it.val)
} }
} }
if signbit {
num = math.Copysign(num, -1)
}
return num, p.typeOfPrimitive(it) return num, p.typeOfPrimitive(it)
} }
var dtTypes = []struct { var dtTypes = []struct {
fmt string fmt string
zone *time.Location zone *time.Location
next bool
}{ }{
{time.RFC3339Nano, time.Local}, {time.RFC3339Nano, time.Local, false},
{"2006-01-02T15:04:05.999999999", internal.LocalDatetime}, {"2006-01-02T15:04:05.999999999", internal.LocalDatetime, false},
{"2006-01-02", internal.LocalDate}, {"2006-01-02", internal.LocalDate, false},
{"15:04:05.999999999", internal.LocalTime}, {"15:04:05.999999999", internal.LocalTime, false},
// tomlNext
{"2006-01-02T15:04Z07:00", time.Local, true},
{"2006-01-02T15:04", internal.LocalDatetime, true},
{"15:04", internal.LocalTime, true},
} }
func (p *parser) valueDatetime(it item) (interface{}, tomlType) { func (p *parser) valueDatetime(it item) (any, tomlType) {
it.val = datetimeRepl.Replace(it.val) it.val = datetimeRepl.Replace(it.val)
var ( var (
t time.Time t time.Time
@ -330,29 +370,49 @@ func (p *parser) valueDatetime(it item) (interface{}, tomlType) {
err error err error
) )
for _, dt := range dtTypes { for _, dt := range dtTypes {
if dt.next && !p.tomlNext {
continue
}
t, err = time.ParseInLocation(dt.fmt, it.val, dt.zone) t, err = time.ParseInLocation(dt.fmt, it.val, dt.zone)
if err == nil { if err == nil {
if missingLeadingZero(it.val, dt.fmt) {
p.panicErr(it, errParseDate{it.val})
}
ok = true ok = true
break break
} }
} }
if !ok { if !ok {
p.panicItemf(it, "Invalid TOML Datetime: %q.", it.val) p.panicErr(it, errParseDate{it.val})
} }
return t, p.typeOfPrimitive(it) return t, p.typeOfPrimitive(it)
} }
func (p *parser) valueArray(it item) (interface{}, tomlType) { // Go's time.Parse() will accept numbers without a leading zero; there isn't any
p.setType(p.currentKey, tomlArray) // way to require it. https://github.com/golang/go/issues/29911
//
// Depend on the fact that the separators (- and :) should always be at the same
// location.
func missingLeadingZero(d, l string) bool {
for i, c := range []byte(l) {
if c == '.' || c == 'Z' {
return false
}
if (c < '0' || c > '9') && d[i] != c {
return true
}
}
return false
}
func (p *parser) valueArray(it item) (any, tomlType) {
p.setType(p.currentKey, tomlArray, it.pos)
// p.setType(p.currentKey, typ)
var ( var (
types []tomlType // Initialize to a non-nil slice to make it consistent with how S = []
// decodes into a non-nil slice inside something like struct { S
// Initialize to a non-nil empty slice. This makes it consistent with // []string }. See #338
// how S = [] decodes into a non-nil slice inside something like struct array = make([]any, 0, 2)
// { S []string }. See #338
array = []interface{}{}
) )
for it = p.next(); it.typ != itemArrayEnd; it = p.next() { for it = p.next(); it.typ != itemArrayEnd; it = p.next() {
if it.typ == itemCommentStart { if it.typ == itemCommentStart {
@ -362,20 +422,20 @@ func (p *parser) valueArray(it item) (interface{}, tomlType) {
val, typ := p.value(it, true) val, typ := p.value(it, true)
array = append(array, val) array = append(array, val)
types = append(types, typ)
// XXX: types isn't used here, we need it to record the accurate type // XXX: type isn't used here, we need it to record the accurate type
// information. // information.
// //
// Not entirely sure how to best store this; could use "key[0]", // Not entirely sure how to best store this; could use "key[0]",
// "key[1]" notation, or maybe store it on the Array type? // "key[1]" notation, or maybe store it on the Array type?
_ = typ
} }
return array, tomlArray return array, tomlArray
} }
func (p *parser) valueInlineTable(it item, parentIsArray bool) (interface{}, tomlType) { func (p *parser) valueInlineTable(it item, parentIsArray bool) (any, tomlType) {
var ( var (
hash = make(map[string]interface{}) topHash = make(map[string]any)
outerContext = p.context outerContext = p.context
outerKey = p.currentKey outerKey = p.currentKey
) )
@ -403,19 +463,33 @@ func (p *parser) valueInlineTable(it item, parentIsArray bool) (interface{}, tom
p.assertEqual(itemKeyEnd, k.typ) p.assertEqual(itemKeyEnd, k.typ)
/// The current key is the last part. /// The current key is the last part.
p.currentKey = key[len(key)-1] p.currentKey = key.last()
/// All the other parts (if any) are the context; need to set each part /// All the other parts (if any) are the context; need to set each part
/// as implicit. /// as implicit.
context := key[:len(key)-1] context := key.parent()
for i := range context { for i := range context {
p.addImplicitContext(append(p.context, context[i:i+1]...)) p.addImplicitContext(append(p.context, context[i:i+1]...))
} }
p.ordered = append(p.ordered, p.context.add(p.currentKey))
/// Set the value. /// Set the value.
val, typ := p.value(p.next(), false) val, typ := p.value(p.next(), false)
p.set(p.currentKey, val, typ) p.setValue(p.currentKey, val)
p.ordered = append(p.ordered, p.context.add(p.currentKey)) p.setType(p.currentKey, typ, it.pos)
hash := topHash
for _, c := range context {
h, ok := hash[c]
if !ok {
h = make(map[string]any)
hash[c] = h
}
hash, ok = h.(map[string]any)
if !ok {
p.panicf("%q is not a table", p.context)
}
}
hash[p.currentKey] = val hash[p.currentKey] = val
/// Restore context. /// Restore context.
@ -423,7 +497,7 @@ func (p *parser) valueInlineTable(it item, parentIsArray bool) (interface{}, tom
} }
p.context = outerContext p.context = outerContext
p.currentKey = outerKey p.currentKey = outerKey
return hash, tomlHash return topHash, tomlHash
} }
// numHasLeadingZero checks if this number has leading zeroes, allowing for '0', // numHasLeadingZero checks if this number has leading zeroes, allowing for '0',
@ -453,9 +527,9 @@ func numUnderscoresOK(s string) bool {
} }
} }
// isHexadecimal is a superset of all the permissable characters // isHexis a superset of all the permissable characters surrounding an
// surrounding an underscore. // underscore.
accept = isHexadecimal(r) accept = isHex(r)
} }
return accept return accept
} }
@ -478,21 +552,19 @@ func numPeriodsOK(s string) bool {
// Establishing the context also makes sure that the key isn't a duplicate, and // Establishing the context also makes sure that the key isn't a duplicate, and
// will create implicit hashes automatically. // will create implicit hashes automatically.
func (p *parser) addContext(key Key, array bool) { func (p *parser) addContext(key Key, array bool) {
var ok bool /// Always start at the top level and drill down for our context.
// Always start at the top level and drill down for our context.
hashContext := p.mapping hashContext := p.mapping
keyContext := make(Key, 0) keyContext := make(Key, 0, len(key)-1)
// We only need implicit hashes for key[0:-1] /// We only need implicit hashes for the parents.
for _, k := range key[0 : len(key)-1] { for _, k := range key.parent() {
_, ok = hashContext[k] _, ok := hashContext[k]
keyContext = append(keyContext, k) keyContext = append(keyContext, k)
// No key? Make an implicit hash and move on. // No key? Make an implicit hash and move on.
if !ok { if !ok {
p.addImplicit(keyContext) p.addImplicit(keyContext)
hashContext[k] = make(map[string]interface{}) hashContext[k] = make(map[string]any)
} }
// If the hash context is actually an array of tables, then set // If the hash context is actually an array of tables, then set
@ -501,9 +573,9 @@ func (p *parser) addContext(key Key, array bool) {
// Otherwise, it better be a table, since this MUST be a key group (by // Otherwise, it better be a table, since this MUST be a key group (by
// virtue of it not being the last element in a key). // virtue of it not being the last element in a key).
switch t := hashContext[k].(type) { switch t := hashContext[k].(type) {
case []map[string]interface{}: case []map[string]any:
hashContext = t[len(t)-1] hashContext = t[len(t)-1]
case map[string]interface{}: case map[string]any:
hashContext = t hashContext = t
default: default:
p.panicf("Key '%s' was already created as a hash.", keyContext) p.panicf("Key '%s' was already created as a hash.", keyContext)
@ -514,39 +586,33 @@ func (p *parser) addContext(key Key, array bool) {
if array { if array {
// If this is the first element for this array, then allocate a new // If this is the first element for this array, then allocate a new
// list of tables for it. // list of tables for it.
k := key[len(key)-1] k := key.last()
if _, ok := hashContext[k]; !ok { if _, ok := hashContext[k]; !ok {
hashContext[k] = make([]map[string]interface{}, 0, 4) hashContext[k] = make([]map[string]any, 0, 4)
} }
// Add a new table. But make sure the key hasn't already been used // Add a new table. But make sure the key hasn't already been used
// for something else. // for something else.
if hash, ok := hashContext[k].([]map[string]interface{}); ok { if hash, ok := hashContext[k].([]map[string]any); ok {
hashContext[k] = append(hash, make(map[string]interface{})) hashContext[k] = append(hash, make(map[string]any))
} else { } else {
p.panicf("Key '%s' was already created and cannot be used as an array.", key) p.panicf("Key '%s' was already created and cannot be used as an array.", key)
} }
} else { } else {
p.setValue(key[len(key)-1], make(map[string]interface{})) p.setValue(key.last(), make(map[string]any))
} }
p.context = append(p.context, key[len(key)-1]) p.context = append(p.context, key.last())
}
// set calls setValue and setType.
func (p *parser) set(key string, val interface{}, typ tomlType) {
p.setValue(key, val)
p.setType(key, typ)
} }
// setValue sets the given key to the given value in the current context. // setValue sets the given key to the given value in the current context.
// It will make sure that the key hasn't already been defined, account for // It will make sure that the key hasn't already been defined, account for
// implicit key groups. // implicit key groups.
func (p *parser) setValue(key string, value interface{}) { func (p *parser) setValue(key string, value any) {
var ( var (
tmpHash interface{} tmpHash any
ok bool ok bool
hash = p.mapping hash = p.mapping
keyContext Key keyContext = make(Key, 0, len(p.context)+1)
) )
for _, k := range p.context { for _, k := range p.context {
keyContext = append(keyContext, k) keyContext = append(keyContext, k)
@ -554,11 +620,11 @@ func (p *parser) setValue(key string, value interface{}) {
p.bug("Context for key '%s' has not been established.", keyContext) p.bug("Context for key '%s' has not been established.", keyContext)
} }
switch t := tmpHash.(type) { switch t := tmpHash.(type) {
case []map[string]interface{}: case []map[string]any:
// The context is a table of hashes. Pick the most recent table // The context is a table of hashes. Pick the most recent table
// defined as the current hash. // defined as the current hash.
hash = t[len(t)-1] hash = t[len(t)-1]
case map[string]interface{}: case map[string]any:
hash = t hash = t
default: default:
p.panicf("Key '%s' has already been defined.", keyContext) p.panicf("Key '%s' has already been defined.", keyContext)
@ -585,9 +651,8 @@ func (p *parser) setValue(key string, value interface{}) {
p.removeImplicit(keyContext) p.removeImplicit(keyContext)
return return
} }
// Otherwise, we have a concrete key trying to override a previous key,
// Otherwise, we have a concrete key trying to override a previous // which is *always* wrong.
// key, which is *always* wrong.
p.panicf("Key '%s' has already been defined.", keyContext) p.panicf("Key '%s' has already been defined.", keyContext)
} }
@ -599,7 +664,7 @@ func (p *parser) setValue(key string, value interface{}) {
// //
// Note that if `key` is empty, then the type given will be applied to the // Note that if `key` is empty, then the type given will be applied to the
// current context (which is either a table or an array of tables). // current context (which is either a table or an array of tables).
func (p *parser) setType(key string, typ tomlType) { func (p *parser) setType(key string, typ tomlType, pos Position) {
keyContext := make(Key, 0, len(p.context)+1) keyContext := make(Key, 0, len(p.context)+1)
keyContext = append(keyContext, p.context...) keyContext = append(keyContext, p.context...)
if len(key) > 0 { // allow type setting for hashes if len(key) > 0 { // allow type setting for hashes
@ -611,19 +676,16 @@ func (p *parser) setType(key string, typ tomlType) {
if len(keyContext) == 0 { if len(keyContext) == 0 {
keyContext = Key{""} keyContext = Key{""}
} }
p.types[keyContext.String()] = typ p.keyInfo[keyContext.String()] = keyInfo{tomlType: typ, pos: pos}
} }
// Implicit keys need to be created when tables are implied in "a.b.c.d = 1" and // Implicit keys need to be created when tables are implied in "a.b.c.d = 1" and
// "[a.b.c]" (the "a", "b", and "c" hashes are never created explicitly). // "[a.b.c]" (the "a", "b", and "c" hashes are never created explicitly).
func (p *parser) addImplicit(key Key) { p.implicits[key.String()] = struct{}{} } func (p *parser) addImplicit(key Key) { p.implicits[key.String()] = struct{}{} }
func (p *parser) removeImplicit(key Key) { delete(p.implicits, key.String()) } func (p *parser) removeImplicit(key Key) { delete(p.implicits, key.String()) }
func (p *parser) isImplicit(key Key) bool { _, ok := p.implicits[key.String()]; return ok } func (p *parser) isImplicit(key Key) bool { _, ok := p.implicits[key.String()]; return ok }
func (p *parser) isArray(key Key) bool { return p.types[key.String()] == tomlArray } func (p *parser) isArray(key Key) bool { return p.keyInfo[key.String()].tomlType == tomlArray }
func (p *parser) addImplicitContext(key Key) { func (p *parser) addImplicitContext(key Key) { p.addImplicit(key); p.addContext(key, false) }
p.addImplicit(key)
p.addContext(key, false)
}
// current returns the full key name of the current context. // current returns the full key name of the current context.
func (p *parser) current() string { func (p *parser) current() string {
@ -646,112 +708,131 @@ func stripFirstNewline(s string) string {
return s return s
} }
// Remove newlines inside triple-quoted strings if a line ends with "\". // stripEscapedNewlines removes whitespace after line-ending backslashes in
func stripEscapedNewlines(s string) string { // multiline strings.
split := strings.Split(s, "\n") //
if len(split) < 1 { // A line-ending backslash is an unescaped \ followed only by whitespace until
return s // the next newline. After a line-ending backslash, all whitespace is removed
} // until the next non-whitespace character.
func (p *parser) stripEscapedNewlines(s string) string {
var (
b strings.Builder
i int
)
b.Grow(len(s))
for {
ix := strings.Index(s[i:], `\`)
if ix < 0 {
b.WriteString(s)
return b.String()
}
i += ix
escNL := false // Keep track of the last non-blank line was escaped. if len(s) > i+1 && s[i+1] == '\\' {
for i, line := range split { // Escaped backslash.
line = strings.TrimRight(line, " \t\r") i += 2
continue
if len(line) == 0 || line[len(line)-1] != '\\' { }
split[i] = strings.TrimRight(split[i], "\r") // Scan until the next non-whitespace.
if !escNL && i != len(split)-1 { j := i + 1
split[i] += "\n" whitespaceLoop:
for ; j < len(s); j++ {
switch s[j] {
case ' ', '\t', '\r', '\n':
default:
break whitespaceLoop
} }
}
if j == i+1 {
// Not a whitespace escape.
i++
continue continue
} }
if !strings.Contains(s[i:j], "\n") {
escBS := true // This is not a line-ending backslash. (It's a bad escape sequence,
for j := len(line) - 1; j >= 0 && line[j] == '\\'; j-- { // but we can let replaceEscapes catch it.)
escBS = !escBS i++
}
if escNL {
line = strings.TrimLeft(line, " \t\r")
}
escNL = !escBS
if escBS {
split[i] += "\n"
continue continue
} }
b.WriteString(s[:i])
split[i] = line[:len(line)-1] // Remove \ s = s[j:]
if len(split)-1 > i { i = 0
split[i+1] = strings.TrimLeft(split[i+1], " \t\r")
}
} }
return strings.Join(split, "")
} }
func (p *parser) replaceEscapes(it item, str string) string { func (p *parser) replaceEscapes(it item, str string) string {
replaced := make([]rune, 0, len(str)) var (
s := []byte(str) b strings.Builder
r := 0 skip = 0
for r < len(s) { )
if s[r] != '\\' { b.Grow(len(str))
c, size := utf8.DecodeRune(s[r:]) for i, c := range str {
r += size if skip > 0 {
replaced = append(replaced, c) skip--
continue continue
} }
r += 1 if c != '\\' {
if r >= len(s) { b.WriteRune(c)
continue
}
if i >= len(str) {
p.bug("Escape sequence at end of string.") p.bug("Escape sequence at end of string.")
return "" return ""
} }
switch s[r] { switch str[i+1] {
default: default:
p.bug("Expected valid escape code after \\, but got %q.", s[r]) p.bug("Expected valid escape code after \\, but got %q.", str[i+1])
return ""
case ' ', '\t': case ' ', '\t':
p.panicItemf(it, "invalid escape: '\\%c'", s[r]) p.panicItemf(it, "invalid escape: '\\%c'", str[i+1])
return ""
case 'b': case 'b':
replaced = append(replaced, rune(0x0008)) b.WriteByte(0x08)
r += 1 skip = 1
case 't': case 't':
replaced = append(replaced, rune(0x0009)) b.WriteByte(0x09)
r += 1 skip = 1
case 'n': case 'n':
replaced = append(replaced, rune(0x000A)) b.WriteByte(0x0a)
r += 1 skip = 1
case 'f': case 'f':
replaced = append(replaced, rune(0x000C)) b.WriteByte(0x0c)
r += 1 skip = 1
case 'r': case 'r':
replaced = append(replaced, rune(0x000D)) b.WriteByte(0x0d)
r += 1 skip = 1
case 'e':
if p.tomlNext {
b.WriteByte(0x1b)
skip = 1
}
case '"': case '"':
replaced = append(replaced, rune(0x0022)) b.WriteByte(0x22)
r += 1 skip = 1
case '\\': case '\\':
replaced = append(replaced, rune(0x005C)) b.WriteByte(0x5c)
r += 1 skip = 1
// The lexer guarantees the correct number of characters are present;
// don't need to check here.
case 'x':
if p.tomlNext {
escaped := p.asciiEscapeToUnicode(it, str[i+2:i+4])
b.WriteRune(escaped)
skip = 3
}
case 'u': case 'u':
// At this point, we know we have a Unicode escape of the form escaped := p.asciiEscapeToUnicode(it, str[i+2:i+6])
// `uXXXX` at [r, r+5). (Because the lexer guarantees this b.WriteRune(escaped)
// for us.) skip = 5
escaped := p.asciiEscapeToUnicode(it, s[r+1:r+5])
replaced = append(replaced, escaped)
r += 5
case 'U': case 'U':
// At this point, we know we have a Unicode escape of the form escaped := p.asciiEscapeToUnicode(it, str[i+2:i+10])
// `uXXXX` at [r, r+9). (Because the lexer guarantees this b.WriteRune(escaped)
// for us.) skip = 9
escaped := p.asciiEscapeToUnicode(it, s[r+1:r+9])
replaced = append(replaced, escaped)
r += 9
} }
} }
return string(replaced) return b.String()
} }
func (p *parser) asciiEscapeToUnicode(it item, bs []byte) rune { func (p *parser) asciiEscapeToUnicode(it item, s string) rune {
s := string(bs)
hex, err := strconv.ParseUint(strings.ToLower(s), 16, 32) hex, err := strconv.ParseUint(strings.ToLower(s), 16, 32)
if err != nil { if err != nil {
p.bug("Could not parse '%s' as a hexadecimal number, but the lexer claims it's OK: %s", s, err) p.bug("Could not parse '%s' as a hexadecimal number, but the lexer claims it's OK: %s", s, err)

View File

@ -25,10 +25,8 @@ type field struct {
// breaking ties with index sequence. // breaking ties with index sequence.
type byName []field type byName []field
func (x byName) Len() int { return len(x) } func (x byName) Len() int { return len(x) }
func (x byName) Swap(i, j int) { x[i], x[j] = x[j], x[i] } func (x byName) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x byName) Less(i, j int) bool { func (x byName) Less(i, j int) bool {
if x[i].name != x[j].name { if x[i].name != x[j].name {
return x[i].name < x[j].name return x[i].name < x[j].name
@ -45,10 +43,8 @@ func (x byName) Less(i, j int) bool {
// byIndex sorts field by index sequence. // byIndex sorts field by index sequence.
type byIndex []field type byIndex []field
func (x byIndex) Len() int { return len(x) } func (x byIndex) Len() int { return len(x) }
func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] } func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x byIndex) Less(i, j int) bool { func (x byIndex) Less(i, j int) bool {
for k, xik := range x[i].index { for k, xik := range x[i].index {
if k >= len(x[j].index) { if k >= len(x[j].index) {

View File

@ -22,13 +22,8 @@ func typeIsTable(t tomlType) bool {
type tomlBaseType string type tomlBaseType string
func (btype tomlBaseType) typeString() string { func (btype tomlBaseType) typeString() string { return string(btype) }
return string(btype) func (btype tomlBaseType) String() string { return btype.typeString() }
}
func (btype tomlBaseType) String() string {
return btype.typeString()
}
var ( var (
tomlInteger tomlBaseType = "Integer" tomlInteger tomlBaseType = "Integer"
@ -54,7 +49,7 @@ func (p *parser) typeOfPrimitive(lexItem item) tomlType {
return tomlFloat return tomlFloat
case itemDatetime: case itemDatetime:
return tomlDatetime return tomlDatetime
case itemString: case itemString, itemStringEsc:
return tomlString return tomlString
case itemMultilineString: case itemMultilineString:
return tomlString return tomlString

View File

@ -6,25 +6,20 @@ linters:
disable-all: true disable-all: true
enable: enable:
- bodyclose - bodyclose
- deadcode
- depguard
- dogsled - dogsled
- goconst - goconst
- gocritic - gocritic
- gofmt - gofmt
- goimports - goimports
- golint
- gosec - gosec
- gosimple - gosimple
- govet - govet
- ineffassign - ineffassign
- interfacer
- maligned
- misspell - misspell
- prealloc - prealloc
- scopelint - exportloopref
- revive
- staticcheck - staticcheck
- structcheck
- stylecheck - stylecheck
- typecheck - typecheck
- unconvert - unconvert

View File

@ -9,18 +9,39 @@ before:
builds: builds:
- env: - env:
- CGO_ENABLED=0 - CGO_ENABLED=0
- >-
{{- if eq .Os "darwin" }}
{{- if eq .Arch "amd64"}}CC=o64-clang{{- end }}
{{- if eq .Arch "arm64"}}CC=aarch64-apple-darwin20.2-clang{{- end }}
{{- end }}
{{- if eq .Os "windows" }}
{{- if eq .Arch "amd64" }}CC=x86_64-w64-mingw32-gcc{{- end }}
{{- end }}
main: ./cmd/escargs main: ./cmd/escargs
goos: goos:
- linux - linux
- windows - windows
- darwin - darwin
archives: - freebsd
- replacements: goarch:
darwin: Darwin - amd64
linux: Linux - arm64
windows: Windows - arm
386: i386 goarm:
amd64: x86_64 - 6
- 7
goamd64:
- v2
- v3
ignore:
- goos: darwin
goarch: 386
- goos: linux
goarch: arm
goarm: 7
- goarm: mips64
- gomips: hardfloat
- goamd64: v4
checksum: checksum:
name_template: 'checksums.txt' name_template: 'checksums.txt'
snapshot: snapshot:

View File

@ -25,9 +25,9 @@ and [much faster][v2-bench]. If you only need reading and writing TOML documents
(majority of cases), those features are implemented and the API unlikely to (majority of cases), those features are implemented and the API unlikely to
change. change.
The remaining features (Document structure editing and tooling) will be added The remaining features will be added shortly. While pull-requests are welcome on
shortly. While pull-requests are welcome on v1, no active development is v1, no active development is expected on it. When v2.0.0 is released, v1 will be
expected on it. When v2.0.0 is released, v1 will be deprecated. deprecated.
👉 [go-toml v2][v2] 👉 [go-toml v2][v2]

19
vendor/github.com/pelletier/go-toml/SECURITY.md generated vendored Normal file
View File

@ -0,0 +1,19 @@
# Security Policy
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ---------- | ------------------ |
| Latest 2.x | :white_check_mark: |
| All 1.x | :x: |
| All 0.x | :x: |
## Reporting a Vulnerability
Email a vulnerability report to `security@pelletier.codes`. Make sure to include
as many details as possible to reproduce the vulnerability. This is a
side-project: I will try to get back to you as quickly as possible, time
permitting in my personal life. Providing a working patch helps very much!

View File

@ -1113,7 +1113,7 @@ func (d *Decoder) valueFromToml(mtype reflect.Type, tval interface{}, mval1 *ref
return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v", tval, tval, mtype.String()) return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v", tval, tval, mtype.String())
} }
if val.Convert(reflect.TypeOf(int(1))).Int() < 0 { if val.Type().Kind() != reflect.Uint64 && val.Convert(reflect.TypeOf(int(1))).Int() < 0 {
return reflect.ValueOf(nil), fmt.Errorf("%v(%T) is negative so does not fit in %v", tval, tval, mtype.String()) return reflect.ValueOf(nil), fmt.Errorf("%v(%T) is negative so does not fit in %v", tval, tval, mtype.String())
} }
if reflect.Indirect(reflect.New(mtype)).OverflowUint(val.Convert(reflect.TypeOf(uint64(0))).Uint()) { if reflect.Indirect(reflect.New(mtype)).OverflowUint(val.Convert(reflect.TypeOf(uint64(0))).Uint()) {

View File

@ -293,42 +293,41 @@ func (p *tomlParser) parseRvalue() interface{} {
return math.NaN() return math.NaN()
case tokenInteger: case tokenInteger:
cleanedVal := cleanupNumberToken(tok.val) cleanedVal := cleanupNumberToken(tok.val)
var err error base := 10
var val int64 s := cleanedVal
checkInvalidUnderscore := numberContainsInvalidUnderscore
if len(cleanedVal) >= 3 && cleanedVal[0] == '0' { if len(cleanedVal) >= 3 && cleanedVal[0] == '0' {
switch cleanedVal[1] { switch cleanedVal[1] {
case 'x': case 'x':
err = hexNumberContainsInvalidUnderscore(tok.val) checkInvalidUnderscore = hexNumberContainsInvalidUnderscore
if err != nil { base = 16
p.raiseError(tok, "%s", err)
}
val, err = strconv.ParseInt(cleanedVal[2:], 16, 64)
case 'o': case 'o':
err = numberContainsInvalidUnderscore(tok.val) base = 8
if err != nil {
p.raiseError(tok, "%s", err)
}
val, err = strconv.ParseInt(cleanedVal[2:], 8, 64)
case 'b': case 'b':
err = numberContainsInvalidUnderscore(tok.val) base = 2
if err != nil {
p.raiseError(tok, "%s", err)
}
val, err = strconv.ParseInt(cleanedVal[2:], 2, 64)
default: default:
panic("invalid base") // the lexer should catch this first panic("invalid base") // the lexer should catch this first
} }
} else { s = cleanedVal[2:]
err = numberContainsInvalidUnderscore(tok.val)
if err != nil {
p.raiseError(tok, "%s", err)
}
val, err = strconv.ParseInt(cleanedVal, 10, 64)
} }
err := checkInvalidUnderscore(tok.val)
if err != nil { if err != nil {
p.raiseError(tok, "%s", err) p.raiseError(tok, "%s", err)
} }
return val
var val interface{}
val, err = strconv.ParseInt(s, base, 64)
if err == nil {
return val
}
if s[0] != '-' {
if val, err = strconv.ParseUint(s, base, 64); err == nil {
return val
}
}
p.raiseError(tok, "%s", err)
case tokenFloat: case tokenFloat:
err := numberContainsInvalidUnderscore(tok.val) err := numberContainsInvalidUnderscore(tok.val)
if err != nil { if err != nil {

View File

@ -471,7 +471,7 @@ func LoadBytes(b []byte) (tree *Tree, err error) {
if _, ok := r.(runtime.Error); ok { if _, ok := r.(runtime.Error); ok {
panic(r) panic(r)
} }
err = errors.New(r.(string)) err = fmt.Errorf("%s", r)
} }
}() }()

13
vendor/modules.txt vendored
View File

@ -2,8 +2,8 @@
## explicit; go 1.16 ## explicit; go 1.16
github.com/Azure/go-ansiterm github.com/Azure/go-ansiterm
github.com/Azure/go-ansiterm/winterm github.com/Azure/go-ansiterm/winterm
# github.com/BurntSushi/toml v1.0.0 # github.com/BurntSushi/toml v1.4.0
## explicit; go 1.16 ## explicit; go 1.18
github.com/BurntSushi/toml github.com/BurntSushi/toml
github.com/BurntSushi/toml/internal github.com/BurntSushi/toml/internal
# github.com/MakeNowJust/heredoc v1.0.0 # github.com/MakeNowJust/heredoc v1.0.0
@ -15,7 +15,7 @@ github.com/NYTimes/gziphandler
# github.com/adhocore/gronx v1.6.3 # github.com/adhocore/gronx v1.6.3
## explicit; go 1.13 ## explicit; go 1.13
github.com/adhocore/gronx github.com/adhocore/gronx
# github.com/alessio/shellescape v1.4.1 # github.com/alessio/shellescape v1.4.2
## explicit; go 1.14 ## explicit; go 1.14
github.com/alessio/shellescape github.com/alessio/shellescape
# github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df # github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df
@ -366,7 +366,7 @@ github.com/opensearch-project/opensearch-go/internal/version
github.com/opensearch-project/opensearch-go/opensearchapi github.com/opensearch-project/opensearch-go/opensearchapi
github.com/opensearch-project/opensearch-go/opensearchtransport github.com/opensearch-project/opensearch-go/opensearchtransport
github.com/opensearch-project/opensearch-go/signer github.com/opensearch-project/opensearch-go/signer
# github.com/pelletier/go-toml v1.9.4 # github.com/pelletier/go-toml v1.9.5
## explicit; go 1.12 ## explicit; go 1.12
github.com/pelletier/go-toml github.com/pelletier/go-toml
# github.com/pelletier/go-toml/v2 v2.1.0 # github.com/pelletier/go-toml/v2 v2.1.0
@ -1847,8 +1847,8 @@ sigs.k8s.io/custom-metrics-apiserver/pkg/registry/external_metrics
## explicit; go 1.18 ## explicit; go 1.18
sigs.k8s.io/json sigs.k8s.io/json
sigs.k8s.io/json/internal/golang/encoding/json sigs.k8s.io/json/internal/golang/encoding/json
# sigs.k8s.io/kind v0.22.0 # sigs.k8s.io/kind v0.24.0
## explicit; go 1.16 ## explicit; go 1.17
sigs.k8s.io/kind/pkg/apis/config/defaults sigs.k8s.io/kind/pkg/apis/config/defaults
sigs.k8s.io/kind/pkg/apis/config/v1alpha4 sigs.k8s.io/kind/pkg/apis/config/v1alpha4
sigs.k8s.io/kind/pkg/cluster sigs.k8s.io/kind/pkg/cluster
@ -1871,6 +1871,7 @@ sigs.k8s.io/kind/pkg/cluster/internal/logs
sigs.k8s.io/kind/pkg/cluster/internal/providers sigs.k8s.io/kind/pkg/cluster/internal/providers
sigs.k8s.io/kind/pkg/cluster/internal/providers/common sigs.k8s.io/kind/pkg/cluster/internal/providers/common
sigs.k8s.io/kind/pkg/cluster/internal/providers/docker sigs.k8s.io/kind/pkg/cluster/internal/providers/docker
sigs.k8s.io/kind/pkg/cluster/internal/providers/nerdctl
sigs.k8s.io/kind/pkg/cluster/internal/providers/podman sigs.k8s.io/kind/pkg/cluster/internal/providers/podman
sigs.k8s.io/kind/pkg/cluster/nodes sigs.k8s.io/kind/pkg/cluster/nodes
sigs.k8s.io/kind/pkg/cluster/nodeutils sigs.k8s.io/kind/pkg/cluster/nodeutils

View File

@ -18,4 +18,4 @@ limitations under the License.
package defaults package defaults
// Image is the default for the Config.Image field, aka the default node image. // Image is the default for the Config.Image field, aka the default node image.
const Image = "kindest/node:v1.29.2@sha256:51a1434a5397193442f0be2a297b488b6c919ce8a3931be0ce822606ea5ca245" const Image = "kindest/node:v1.31.0@sha256:53df588e04085fd41ae12de0c3fe4c72f7013bba32a20e7325357a1ac94ba865"

View File

@ -186,7 +186,7 @@ type Networking struct {
// If DisableDefaultCNI is true, kind will not install the default CNI setup. // If DisableDefaultCNI is true, kind will not install the default CNI setup.
// Instead the user should install their own CNI after creating the cluster. // Instead the user should install their own CNI after creating the cluster.
DisableDefaultCNI bool `yaml:"disableDefaultCNI,omitempty" json:"disableDefaultCNI,omitempty"` DisableDefaultCNI bool `yaml:"disableDefaultCNI,omitempty" json:"disableDefaultCNI,omitempty"`
// KubeProxyMode defines if kube-proxy should operate in iptables or ipvs mode // KubeProxyMode defines if kube-proxy should operate in iptables, ipvs or nftables mode
// Defaults to 'iptables' mode // Defaults to 'iptables' mode
KubeProxyMode ProxyMode `yaml:"kubeProxyMode,omitempty" json:"kubeProxyMode,omitempty"` KubeProxyMode ProxyMode `yaml:"kubeProxyMode,omitempty" json:"kubeProxyMode,omitempty"`
// DNSSearch defines the DNS search domain to use for nodes. If not set, this will be inherited from the host. // DNSSearch defines the DNS search domain to use for nodes. If not set, this will be inherited from the host.
@ -213,6 +213,8 @@ const (
IPTablesProxyMode ProxyMode = "iptables" IPTablesProxyMode ProxyMode = "iptables"
// IPVSProxyMode sets ProxyMode to ipvs // IPVSProxyMode sets ProxyMode to ipvs
IPVSProxyMode ProxyMode = "ipvs" IPVSProxyMode ProxyMode = "ipvs"
// NFTablesProxyMode sets ProxyMode to nftables
NFTablesProxyMode ProxyMode = "nftables"
) )
// PatchJSON6902 represents an inline kustomize json 6902 patch // PatchJSON6902 represents an inline kustomize json 6902 patch

View File

@ -60,24 +60,38 @@ func (a *action) Execute(ctx *actions.ActionContext) error {
return err return err
} }
// skip preflight checks, as these have undesirable side effects kubeVersionStr, err := nodeutils.KubeVersion(node)
// and don't tell us much. requires kubeadm 1.13+ if err != nil {
skipPhases := "preflight" return errors.Wrap(err, "failed to get kubernetes version from node")
if a.skipKubeProxy { }
skipPhases += ",addon/kube-proxy" kubeVersion, err := version.ParseGeneric(kubeVersionStr)
if err != nil {
return errors.Wrapf(err, "failed to parse kubernetes version %q", kubeVersionStr)
} }
// run kubeadm args := []string{
cmd := node.Command(
// init because this is the control plane node // init because this is the control plane node
"kubeadm", "init", "init",
"--skip-phases="+skipPhases,
// specify our generated config file // specify our generated config file
"--config=/kind/kubeadm.conf", "--config=/kind/kubeadm.conf",
"--skip-token-print", "--skip-token-print",
// increase verbosity for debugging // increase verbosity for debugging
"--v=6", "--v=6",
) }
// Newer versions set this in the config file.
if kubeVersion.LessThan(version.MustParseSemantic("v1.23.0")) {
// Skip preflight to avoid pulling images.
// Kind pre-pulls images and preflight may conflict with that.
skipPhases := "preflight"
if a.skipKubeProxy {
skipPhases += ",addon/kube-proxy"
}
args = append(args, "--skip-phases="+skipPhases)
}
// run kubeadm
cmd := node.Command("kubeadm", args...)
lines, err := exec.CombinedOutputLines(cmd) lines, err := exec.CombinedOutputLines(cmd)
ctx.Logger.V(3).Info(strings.Join(lines, "\n")) ctx.Logger.V(3).Info(strings.Join(lines, "\n"))
if err != nil { if err != nil {

View File

@ -24,6 +24,7 @@ import (
"sigs.k8s.io/kind/pkg/cluster/nodes" "sigs.k8s.io/kind/pkg/cluster/nodes"
"sigs.k8s.io/kind/pkg/errors" "sigs.k8s.io/kind/pkg/errors"
"sigs.k8s.io/kind/pkg/exec" "sigs.k8s.io/kind/pkg/exec"
"sigs.k8s.io/kind/pkg/internal/version"
"sigs.k8s.io/kind/pkg/log" "sigs.k8s.io/kind/pkg/log"
"sigs.k8s.io/kind/pkg/cluster/nodeutils" "sigs.k8s.io/kind/pkg/cluster/nodeutils"
@ -117,18 +118,31 @@ func joinWorkers(
// runKubeadmJoin executes kubeadm join command // runKubeadmJoin executes kubeadm join command
func runKubeadmJoin(logger log.Logger, node nodes.Node) error { func runKubeadmJoin(logger log.Logger, node nodes.Node) error {
// run kubeadm join kubeVersionStr, err := nodeutils.KubeVersion(node)
// TODO(bentheelder): this should be using the config file if err != nil {
cmd := node.Command( return errors.Wrap(err, "failed to get kubernetes version from node")
"kubeadm", "join", }
kubeVersion, err := version.ParseGeneric(kubeVersionStr)
if err != nil {
return errors.Wrapf(err, "failed to parse kubernetes version %q", kubeVersionStr)
}
args := []string{
"join",
// the join command uses the config file generated in a well known location // the join command uses the config file generated in a well known location
"--config", "/kind/kubeadm.conf", "--config", "/kind/kubeadm.conf",
// skip preflight checks, as these have undesirable side effects
// and don't tell us much. requires kubeadm 1.13+
"--skip-phases=preflight",
// increase verbosity for debugging // increase verbosity for debugging
"--v=6", "--v=6",
) }
// Newer versions set this in the config file.
if kubeVersion.LessThan(version.MustParseSemantic("v1.23.0")) {
// Skip preflight to avoid pulling images.
// Kind pre-pulls images and preflight may conflict with that.
args = append(args, "--skip-phases=preflight")
}
// run kubeadm join
cmd := node.Command("kubeadm", args...)
lines, err := exec.CombinedOutputLines(cmd) lines, err := exec.CombinedOutputLines(cmd)
logger.V(3).Info(strings.Join(lines, "\n")) logger.V(3).Info(strings.Join(lines, "\n"))
if err != nil { if err != nil {

View File

@ -57,7 +57,7 @@ type ConfigData struct {
// The Token for TLS bootstrap // The Token for TLS bootstrap
Token string Token string
// KubeProxyMode defines the kube-proxy mode between iptables or ipvs // KubeProxyMode defines the kube-proxy mode between iptables, ipvs or nftables
KubeProxyMode string KubeProxyMode string
// The subnet used for pods // The subnet used for pods
PodSubnet string PodSubnet string
@ -79,10 +79,6 @@ type ConfigData struct {
// RootlessProvider is true if kind is running with rootless mode // RootlessProvider is true if kind is running with rootless mode
RootlessProvider bool RootlessProvider bool
// DisableLocalStorageCapacityIsolation is typically set true based on RootlessProvider
// based on the Kubernetes version, if true kubelet localStorageCapacityIsolation is set false
DisableLocalStorageCapacityIsolation bool
// DerivedConfigData contains fields computed from the other fields for use // DerivedConfigData contains fields computed from the other fields for use
// in the config templates and should only be populated by calling Derive() // in the config templates and should only be populated by calling Derive()
DerivedConfigData DerivedConfigData
@ -107,6 +103,10 @@ type DerivedConfigData struct {
IPv6 bool IPv6 bool
// kubelet cgroup driver, based on kubernetes version // kubelet cgroup driver, based on kubernetes version
CgroupDriver string CgroupDriver string
// JoinSkipPhases are the skipPhases values for the JoinConfiguration.
JoinSkipPhases []string
// InitSkipPhases are the skipPhases values for the InitConfiguration.
InitSkipPhases []string
} }
type FeatureGate struct { type FeatureGate struct {
@ -166,6 +166,15 @@ func (c *ConfigData) Derive() {
runtimeConfig = append(runtimeConfig, fmt.Sprintf("%s=%s", k, v)) runtimeConfig = append(runtimeConfig, fmt.Sprintf("%s=%s", k, v))
} }
c.RuntimeConfigString = strings.Join(runtimeConfig, ",") c.RuntimeConfigString = strings.Join(runtimeConfig, ",")
// Skip preflight to avoid pulling images.
// Kind pre-pulls images and preflight may conflict with that.
// requires kubeadm 1.22+
c.JoinSkipPhases = []string{"preflight"}
c.InitSkipPhases = []string{"preflight"}
if c.KubeProxyMode == string(config.NoneProxyMode) {
c.InitSkipPhases = append(c.InitSkipPhases, "addon/kube-proxy")
}
} }
// See docs for these APIs at: // See docs for these APIs at:
@ -285,7 +294,7 @@ evictionHard:
{{ range $index, $gate := .SortedFeatureGates }} {{ range $index, $gate := .SortedFeatureGates }}
"{{ (StructuralData $gate.Name) }}": {{ $gate.Value }} "{{ (StructuralData $gate.Name) }}": {{ $gate.Value }}
{{end}}{{end}} {{end}}{{end}}
{{if ne .KubeProxyMode "None"}} {{if ne .KubeProxyMode "none"}}
--- ---
apiVersion: kubeproxy.config.k8s.io/v1alpha1 apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration kind: KubeProxyConfiguration
@ -302,6 +311,12 @@ conntrack:
# Skip setting sysctl value "net.netfilter.nf_conntrack_max" # Skip setting sysctl value "net.netfilter.nf_conntrack_max"
# It is a global variable that affects other namespaces # It is a global variable that affects other namespaces
maxPerCore: 0 maxPerCore: 0
# Set sysctl value "net.netfilter.nf_conntrack_tcp_be_liberal"
# for nftables proxy (theoretically for kernels older than 6.1)
# xref: https://github.com/kubernetes/kubernetes/issues/117924
{{if and (eq .KubeProxyMode "nftables") (not .RootlessProvider)}}
tcpBeLiberal: true
{{end}}
{{if .RootlessProvider}} {{if .RootlessProvider}}
# Skip setting "net.netfilter.nf_conntrack_tcp_timeout_established" # Skip setting "net.netfilter.nf_conntrack_tcp_timeout_established"
tcpEstablishedTimeout: 0s tcpEstablishedTimeout: 0s
@ -374,6 +389,12 @@ nodeRegistration:
node-ip: "{{ .NodeAddress }}" node-ip: "{{ .NodeAddress }}"
provider-id: "kind://{{.NodeProvider}}/{{.ClusterName}}/{{.NodeName}}" provider-id: "kind://{{.NodeProvider}}/{{.ClusterName}}/{{.NodeName}}"
node-labels: "{{ .NodeLabels }}" node-labels: "{{ .NodeLabels }}"
{{ if .InitSkipPhases -}}
skipPhases:
{{- range $phase := .InitSkipPhases }}
- "{{ $phase }}"
{{- end }}
{{- end }}
--- ---
# no-op entry that exists solely so it can be patched # no-op entry that exists solely so it can be patched
apiVersion: kubeadm.k8s.io/v1beta3 apiVersion: kubeadm.k8s.io/v1beta3
@ -397,6 +418,12 @@ discovery:
apiServerEndpoint: "{{ .ControlPlaneEndpoint }}" apiServerEndpoint: "{{ .ControlPlaneEndpoint }}"
token: "{{ .Token }}" token: "{{ .Token }}"
unsafeSkipCAVerification: true unsafeSkipCAVerification: true
{{ if .JoinSkipPhases -}}
skipPhases:
{{ range $phase := .JoinSkipPhases -}}
- "{{ $phase }}"
{{- end }}
{{- end }}
--- ---
apiVersion: kubelet.config.k8s.io/v1beta1 apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration kind: KubeletConfiguration
@ -422,8 +449,7 @@ evictionHard:
{{ range $index, $gate := .SortedFeatureGates }} {{ range $index, $gate := .SortedFeatureGates }}
"{{ (StructuralData $gate.Name) }}": {{ $gate.Value }} "{{ (StructuralData $gate.Name) }}": {{ $gate.Value }}
{{end}}{{end}} {{end}}{{end}}
{{if .DisableLocalStorageCapacityIsolation}}localStorageCapacityIsolation: false{{end}} {{if ne .KubeProxyMode "none"}}
{{if ne .KubeProxyMode "None"}}
--- ---
apiVersion: kubeproxy.config.k8s.io/v1alpha1 apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration kind: KubeProxyConfiguration
@ -440,6 +466,12 @@ conntrack:
# Skip setting sysctl value "net.netfilter.nf_conntrack_max" # Skip setting sysctl value "net.netfilter.nf_conntrack_max"
# It is a global variable that affects other namespaces # It is a global variable that affects other namespaces
maxPerCore: 0 maxPerCore: 0
# Set sysctl value "net.netfilter.nf_conntrack_tcp_be_liberal"
# for nftables proxy (theoretically for kernels older than 6.1)
# xref: https://github.com/kubernetes/kubernetes/issues/117924
{{if and (eq .KubeProxyMode "nftables") (not .RootlessProvider)}}
tcpBeLiberal: true
{{end}}
{{if .RootlessProvider}} {{if .RootlessProvider}}
# Skip setting "net.netfilter.nf_conntrack_tcp_timeout_established" # Skip setting "net.netfilter.nf_conntrack_tcp_timeout_established"
tcpEstablishedTimeout: 0s tcpEstablishedTimeout: 0s
@ -468,16 +500,6 @@ func Config(data ConfigData) (config string, err error) {
return "", errors.Errorf("version %q is not compatible with rootless provider (hint: kind v0.11.x may work with this version)", ver) return "", errors.Errorf("version %q is not compatible with rootless provider (hint: kind v0.11.x may work with this version)", ver)
} }
data.FeatureGates["KubeletInUserNamespace"] = true data.FeatureGates["KubeletInUserNamespace"] = true
// For avoiding err="failed to get rootfs info: failed to get device for dir \"/var/lib/kubelet\": could not find device with major: 0, minor: 41 in cached partitions map"
// https://github.com/kubernetes-sigs/kind/issues/2524
if ver.LessThan(version.MustParseSemantic("v1.25.0-alpha.3.440+0064010cddfa00")) {
// this feature gate was removed in v1.25 and replaced by an opt-out to disable
data.FeatureGates["LocalStorageCapacityIsolation"] = false
} else {
// added in v1.25 https://github.com/kubernetes/kubernetes/pull/111513
data.DisableLocalStorageCapacityIsolation = true
}
} }
// assume the latest API version, then fallback if the k8s version is too low // assume the latest API version, then fallback if the k8s version is too low

View File

@ -403,10 +403,7 @@ func generatePortMappings(clusterIPFamily config.ClusterIPFamily, portMappings .
} }
func createContainer(name string, args []string) error { func createContainer(name string, args []string) error {
if err := exec.Command("docker", append([]string{"run", "--name", name}, args...)...).Run(); err != nil { return exec.Command("docker", append([]string{"run", "--name", name}, args...)...).Run()
return err
}
return nil
} }
func createContainerWithWaitUntilSystemdReachesMultiUserSystem(name string, args []string) error { func createContainerWithWaitUntilSystemdReachesMultiUserSystem(name string, args []string) error {

View File

@ -0,0 +1,2 @@
labels:
- area/provider/nerdctl

View File

@ -0,0 +1,24 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or impliep.
See the License for the specific language governing permissions and
limitations under the License.
*/
package nerdctl
// clusterLabelKey is applied to each "node" container for identification
const clusterLabelKey = "io.x-k8s.kind.cluster"
// nodeRoleLabelKey is applied to each "node" container for categorization
// of nodes by role
const nodeRoleLabelKey = "io.x-k8s.kind.role"

View File

@ -0,0 +1,91 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package nerdctl
import (
"fmt"
"strings"
"time"
"sigs.k8s.io/kind/pkg/errors"
"sigs.k8s.io/kind/pkg/exec"
"sigs.k8s.io/kind/pkg/log"
"sigs.k8s.io/kind/pkg/cluster/internal/providers/common"
"sigs.k8s.io/kind/pkg/internal/apis/config"
"sigs.k8s.io/kind/pkg/internal/cli"
)
// ensureNodeImages ensures that the node images used by the create
// configuration are present
func ensureNodeImages(logger log.Logger, status *cli.Status, cfg *config.Cluster, binaryName string) error {
// pull each required image
for _, image := range common.RequiredNodeImages(cfg).List() {
// prints user friendly message
friendlyImageName, image := sanitizeImage(image)
status.Start(fmt.Sprintf("Ensuring node image (%s) 🖼", friendlyImageName))
if _, err := pullIfNotPresent(logger, image, 4, binaryName); err != nil {
status.End(false)
return err
}
}
return nil
}
// pullIfNotPresent will pull an image if it is not present locally
// retrying up to retries times
// it returns true if it attempted to pull, and any errors from pulling
func pullIfNotPresent(logger log.Logger, image string, retries int, binaryName string) (pulled bool, err error) {
// TODO(bentheelder): switch most (all) of the logging here to debug level
// once we have configurable log levels
// if this did not return an error, then the image exists locally
cmd := exec.Command(binaryName, "inspect", "--type=image", image)
if err := cmd.Run(); err == nil {
logger.V(1).Infof("Image: %s present locally", image)
return false, nil
}
// otherwise try to pull it
return true, pull(logger, image, retries, binaryName)
}
// pull pulls an image, retrying up to retries times
func pull(logger log.Logger, image string, retries int, binaryName string) error {
logger.V(1).Infof("Pulling image: %s ...", image)
err := exec.Command(binaryName, "pull", image).Run()
// retry pulling up to retries times if necessary
if err != nil {
for i := 0; i < retries; i++ {
time.Sleep(time.Second * time.Duration(i+1))
logger.V(1).Infof("Trying again to pull image: %q ... %v", image, err)
// TODO(bentheelder): add some backoff / sleep?
err = exec.Command(binaryName, "pull", image).Run()
if err == nil {
break
}
}
}
return errors.Wrapf(err, "failed to pull image %q", image)
}
// sanitizeImage is a helper to return human readable image name and
// the docker pullable image name from the provided image
func sanitizeImage(image string) (string, string) {
if strings.Contains(image, "@sha256:") {
return strings.Split(image, "@sha256:")[0], image
}
return image, image
}

View File

@ -0,0 +1,187 @@
/*
Copyright 2020 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package nerdctl
import (
"crypto/sha1"
"encoding/binary"
"fmt"
"net"
"strconv"
"strings"
"sigs.k8s.io/kind/pkg/errors"
"sigs.k8s.io/kind/pkg/exec"
)
// This may be overridden by KIND_EXPERIMENTAL_DOCKER_NETWORK env,
// experimentally...
//
// By default currently picking a single network is equivalent to the previous
// behavior *except* that we moved from the default bridge to a user defined
// network because the default bridge is actually special versus any other
// docker network and lacks the embedded DNS
//
// For now this also makes it easier for apps to join the same network, and
// leaves users with complex networking desires to create and manage their own
// networks.
const fixedNetworkName = "kind"
// ensureNetwork checks if docker network by name exists, if not it creates it
func ensureNetwork(name, binaryName string) error {
// check if network exists already and remove any duplicate networks
exists, err := checkIfNetworkExists(name, binaryName)
if err != nil {
return err
}
// network already exists, we're good
// TODO: the network might already exist and not have ipv6 ... :|
// discussion: https://github.com/kubernetes-sigs/kind/pull/1508#discussion_r414594198
if exists {
return nil
}
subnet := generateULASubnetFromName(name, 0)
mtu := getDefaultNetworkMTU(binaryName)
err = createNetwork(name, subnet, mtu, binaryName)
if err == nil {
// Success!
return nil
}
// On the first try check if ipv6 fails entirely on this machine
// https://github.com/kubernetes-sigs/kind/issues/1544
// Otherwise if it's not a pool overlap error, fail
// If it is, make more attempts below
if isIPv6UnavailableError(err) {
// only one attempt, IPAM is automatic in ipv4 only
return createNetwork(name, "", mtu, binaryName)
}
if isPoolOverlapError(err) {
// pool overlap suggests perhaps another process created the network
// check if network exists already and remove any duplicate networks
exists, err := checkIfNetworkExists(name, binaryName)
if err != nil {
return err
}
if exists {
return nil
}
// otherwise we'll start trying with different subnets
} else {
// unknown error ...
return err
}
// keep trying for ipv6 subnets
const maxAttempts = 5
for attempt := int32(1); attempt < maxAttempts; attempt++ {
subnet := generateULASubnetFromName(name, attempt)
err = createNetwork(name, subnet, mtu, binaryName)
if err == nil {
// success!
return nil
}
if isPoolOverlapError(err) {
// pool overlap suggests perhaps another process created the network
// check if network exists already and remove any duplicate networks
exists, err := checkIfNetworkExists(name, binaryName)
if err != nil {
return err
}
if exists {
return nil
}
// otherwise we'll try again
continue
}
// unknown error ...
return err
}
return errors.New("exhausted attempts trying to find a non-overlapping subnet")
}
func createNetwork(name, ipv6Subnet string, mtu int, binaryName string) error {
args := []string{"network", "create", "-d=bridge"}
// TODO: Not supported in nerdctl yet
// "-o", "com.docker.network.bridge.enable_ip_masquerade=true",
if mtu > 0 {
args = append(args, "-o", fmt.Sprintf("com.docker.network.driver.mtu=%d", mtu))
}
if ipv6Subnet != "" {
args = append(args, "--ipv6", "--subnet", ipv6Subnet)
}
args = append(args, name)
return exec.Command(binaryName, args...).Run()
}
// getDefaultNetworkMTU obtains the MTU from the docker default network
func getDefaultNetworkMTU(binaryName string) int {
cmd := exec.Command(binaryName, "network", "inspect", "bridge",
"-f", `{{ index .Options "com.docker.network.driver.mtu" }}`)
lines, err := exec.OutputLines(cmd)
if err != nil || len(lines) != 1 {
return 0
}
mtu, err := strconv.Atoi(lines[0])
if err != nil {
return 0
}
return mtu
}
func checkIfNetworkExists(name, binaryName string) (bool, error) {
out, err := exec.Output(exec.Command(
binaryName, "network", "inspect",
name, "--format={{.Name}}",
))
if err != nil {
return false, nil
}
return strings.HasPrefix(string(out), name), err
}
func isIPv6UnavailableError(err error) bool {
rerr := exec.RunErrorForError(err)
return rerr != nil && strings.HasPrefix(string(rerr.Output), "Error response from daemon: Cannot read IPv6 setup for bridge")
}
func isPoolOverlapError(err error) bool {
rerr := exec.RunErrorForError(err)
return rerr != nil && strings.HasPrefix(string(rerr.Output), "Error response from daemon: Pool overlaps with other one on this address space") || strings.Contains(string(rerr.Output), "networks have overlapping")
}
// generateULASubnetFromName generate an IPv6 subnet based on the
// name and Nth probing attempt
func generateULASubnetFromName(name string, attempt int32) string {
ip := make([]byte, 16)
ip[0] = 0xfc
ip[1] = 0x00
h := sha1.New()
_, _ = h.Write([]byte(name))
_ = binary.Write(h, binary.LittleEndian, attempt)
bs := h.Sum(nil)
for i := 2; i < 8; i++ {
ip[i] = bs[i]
}
subnet := &net.IPNet{
IP: net.IP(ip),
Mask: net.CIDRMask(64, 128),
}
return subnet.String()
}

View File

@ -0,0 +1,175 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or impliep.
See the License for the specific language governing permissions and
limitations under the License.
*/
package nerdctl
import (
"context"
"fmt"
"io"
"strings"
"sigs.k8s.io/kind/pkg/errors"
"sigs.k8s.io/kind/pkg/exec"
)
// nodes.Node implementation for the docker provider
type node struct {
name string
binaryName string
}
func (n *node) String() string {
return n.name
}
func (n *node) Role() (string, error) {
cmd := exec.Command(n.binaryName, "inspect",
"--format", fmt.Sprintf(`{{ index .Config.Labels "%s"}}`, nodeRoleLabelKey),
n.name,
)
lines, err := exec.OutputLines(cmd)
if err != nil {
return "", errors.Wrap(err, "failed to get role for node")
}
if len(lines) != 1 {
return "", errors.Errorf("failed to get role for node: output lines %d != 1", len(lines))
}
return lines[0], nil
}
func (n *node) IP() (ipv4 string, ipv6 string, err error) {
// retrieve the IP address of the node using docker inspect
cmd := exec.Command(n.binaryName, "inspect",
"-f", "{{range .NetworkSettings.Networks}}{{.IPAddress}},{{.GlobalIPv6Address}}{{end}}",
n.name, // ... against the "node" container
)
lines, err := exec.OutputLines(cmd)
if err != nil {
return "", "", errors.Wrap(err, "failed to get container details")
}
if len(lines) != 1 {
return "", "", errors.Errorf("file should only be one line, got %d lines", len(lines))
}
ips := strings.Split(lines[0], ",")
if len(ips) != 2 {
return "", "", errors.Errorf("container addresses should have 2 values, got %d values", len(ips))
}
return ips[0], ips[1], nil
}
func (n *node) Command(command string, args ...string) exec.Cmd {
return &nodeCmd{
binaryName: n.binaryName,
nameOrID: n.name,
command: command,
args: args,
}
}
func (n *node) CommandContext(ctx context.Context, command string, args ...string) exec.Cmd {
return &nodeCmd{
binaryName: n.binaryName,
nameOrID: n.name,
command: command,
args: args,
ctx: ctx,
}
}
// nodeCmd implements exec.Cmd for docker nodes
type nodeCmd struct {
binaryName string
nameOrID string // the container name or ID
command string
args []string
env []string
stdin io.Reader
stdout io.Writer
stderr io.Writer
ctx context.Context
}
func (c *nodeCmd) Run() error {
args := []string{
"exec",
// run with privileges so we can remount etc..
// this might not make sense in the most general sense, but it is
// important to many kind commands
"--privileged",
}
if c.stdin != nil {
args = append(args,
"-i", // interactive so we can supply input
)
}
// set env
for _, env := range c.env {
args = append(args, "-e", env)
}
// specify the container and command, after this everything will be
// args the command in the container rather than to docker
args = append(
args,
c.nameOrID, // ... against the container
c.command, // with the command specified
)
args = append(
args,
// finally, with the caller args
c.args...,
)
var cmd exec.Cmd
if c.ctx != nil {
cmd = exec.CommandContext(c.ctx, c.binaryName, args...)
} else {
cmd = exec.Command(c.binaryName, args...)
}
if c.stdin != nil {
cmd.SetStdin(c.stdin)
}
if c.stderr != nil {
cmd.SetStderr(c.stderr)
}
if c.stdout != nil {
cmd.SetStdout(c.stdout)
}
return cmd.Run()
}
func (c *nodeCmd) SetEnv(env ...string) exec.Cmd {
c.env = env
return c
}
func (c *nodeCmd) SetStdin(r io.Reader) exec.Cmd {
c.stdin = r
return c
}
func (c *nodeCmd) SetStdout(w io.Writer) exec.Cmd {
c.stdout = w
return c
}
func (c *nodeCmd) SetStderr(w io.Writer) exec.Cmd {
c.stderr = w
return c
}
func (n *node) SerialLogs(w io.Writer) error {
return exec.Command(n.binaryName, "logs", n.name).SetStdout(w).SetStderr(w).Run()
}

View File

@ -0,0 +1,392 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or impliep.
See the License for the specific language governing permissions and
limitations under the License.
*/
package nerdctl
import (
"encoding/csv"
"encoding/json"
"fmt"
"net"
osexec "os/exec"
"path/filepath"
"strings"
"sigs.k8s.io/kind/pkg/cluster/nodes"
"sigs.k8s.io/kind/pkg/errors"
"sigs.k8s.io/kind/pkg/exec"
"sigs.k8s.io/kind/pkg/log"
internallogs "sigs.k8s.io/kind/pkg/cluster/internal/logs"
"sigs.k8s.io/kind/pkg/cluster/internal/providers"
"sigs.k8s.io/kind/pkg/cluster/internal/providers/common"
"sigs.k8s.io/kind/pkg/cluster/nodeutils"
"sigs.k8s.io/kind/pkg/internal/apis/config"
"sigs.k8s.io/kind/pkg/internal/cli"
"sigs.k8s.io/kind/pkg/internal/sets"
)
// NewProvider returns a new provider based on executing `nerdctl ...`
func NewProvider(logger log.Logger, binaryName string) providers.Provider {
// if binaryName is unset, do a lookup; we may be here via a
// library call to provider.DetectNodeProvider(), which returns
// true from nerdctl.IsAvailable() by checking for both finch
// and nerdctl. If we don't redo the lookup here, then a finch
// install that triggered IsAvailable() to be true would fail
// to be used if we default to nerdctl when unset.
if binaryName == "" {
// default to "nerdctl"; but look for "finch" if
// nerctl binary lookup fails
binaryName = "nerdctl"
if _, err := osexec.LookPath("nerdctl"); err != nil {
if _, err := osexec.LookPath("finch"); err == nil {
binaryName = "finch"
}
}
}
return &provider{
logger: logger,
binaryName: binaryName,
}
}
// Provider implements provider.Provider
// see NewProvider
type provider struct {
logger log.Logger
binaryName string
info *providers.ProviderInfo
}
// String implements fmt.Stringer
// NOTE: the value of this should not currently be relied upon for anything!
// This is only used for setting the Node's providerID
func (p *provider) String() string {
return "nerdctl"
}
func (p *provider) Binary() string {
return p.binaryName
}
// Provision is part of the providers.Provider interface
func (p *provider) Provision(status *cli.Status, cfg *config.Cluster) (err error) {
// TODO: validate cfg
// ensure node images are pulled before actually provisioning
if err := ensureNodeImages(p.logger, status, cfg, p.Binary()); err != nil {
return err
}
// ensure the pre-requisite network exists
if err := ensureNetwork(fixedNetworkName, p.Binary()); err != nil {
return errors.Wrap(err, "failed to ensure nerdctl network")
}
// actually provision the cluster
icons := strings.Repeat("📦 ", len(cfg.Nodes))
status.Start(fmt.Sprintf("Preparing nodes %s", icons))
defer func() { status.End(err == nil) }()
// plan creating the containers
createContainerFuncs, err := planCreation(cfg, fixedNetworkName, p.Binary())
if err != nil {
return err
}
// actually create nodes
// TODO: remove once nerdctl handles concurrency better
// xref: https://github.com/containerd/nerdctl/issues/2908
for _, f := range createContainerFuncs {
if err := f(); err != nil {
return err
}
}
return nil
}
// ListClusters is part of the providers.Provider interface
func (p *provider) ListClusters() ([]string, error) {
cmd := exec.Command(p.Binary(),
"ps",
"-a", // show stopped nodes
// filter for nodes with the cluster label
"--filter", "label="+clusterLabelKey,
// format to include the cluster name
"--format", fmt.Sprintf(`{{index .Labels "%s"}}`, clusterLabelKey),
)
lines, err := exec.OutputLines(cmd)
if err != nil {
return nil, errors.Wrap(err, "failed to list clusters")
}
return sets.NewString(lines...).List(), nil
}
// ListNodes is part of the providers.Provider interface
func (p *provider) ListNodes(cluster string) ([]nodes.Node, error) {
cmd := exec.Command(p.Binary(),
"ps",
"-a", // show stopped nodes
// filter for nodes with the cluster label
"--filter", fmt.Sprintf("label=%s=%s", clusterLabelKey, cluster),
// format to include the cluster name
"--format", `{{.Names}}`,
)
lines, err := exec.OutputLines(cmd)
if err != nil {
return nil, errors.Wrap(err, "failed to list nodes")
}
length := len(lines)
// convert names to node handles
ret := make([]nodes.Node, 0, length)
for _, name := range lines {
if name != "" {
ret = append(ret, p.node(name))
}
}
return ret, nil
}
// DeleteNodes is part of the providers.Provider interface
func (p *provider) DeleteNodes(n []nodes.Node) error {
if len(n) == 0 {
return nil
}
argsNoRestart := make([]string, 0, len(n)+2)
argsNoRestart = append(argsNoRestart,
"update",
"--restart=no",
)
argsStop := make([]string, 0, len(n)+1)
argsStop = append(argsStop, "stop")
argsWait := make([]string, 0, len(n)+1)
argsWait = append(argsWait, "wait")
argsRm := make([]string, 0, len(n)+3) // allocate once
argsRm = append(argsRm,
"rm",
"-f",
"-v", // delete volumes
)
for _, node := range n {
argsRm = append(argsRm, node.String())
argsStop = append(argsStop, node.String())
argsWait = append(argsWait, node.String())
argsNoRestart = append(argsNoRestart, node.String())
}
if err := exec.Command(p.Binary(), argsNoRestart...).Run(); err != nil {
return errors.Wrap(err, "failed to update restart policy to 'no'")
}
if err := exec.Command(p.Binary(), argsStop...).Run(); err != nil {
return errors.Wrap(err, "failed to stop nodes")
}
if err := exec.Command(p.Binary(), argsWait...).Run(); err != nil {
return errors.Wrap(err, "failed to wait for node exit")
}
if err := exec.Command(p.Binary(), argsRm...).Run(); err != nil {
return errors.Wrap(err, "failed to delete nodes")
}
return nil
}
// GetAPIServerEndpoint is part of the providers.Provider interface
func (p *provider) GetAPIServerEndpoint(cluster string) (string, error) {
// locate the node that hosts this
allNodes, err := p.ListNodes(cluster)
if err != nil {
return "", errors.Wrap(err, "failed to list nodes")
}
n, err := nodeutils.APIServerEndpointNode(allNodes)
if err != nil {
return "", errors.Wrap(err, "failed to get api server endpoint")
}
// if the 'desktop.docker.io/ports/<PORT>/tcp' label is present,
// defer to its value for the api server endpoint
//
// For example:
// "Labels": {
// "desktop.docker.io/ports/6443/tcp": "10.0.1.7:6443",
// }
cmd := exec.Command(
p.Binary(), "inspect",
"--format", fmt.Sprintf(
"{{ index .Config.Labels \"desktop.docker.io/ports/%d/tcp\" }}", common.APIServerInternalPort,
),
n.String(),
)
lines, err := exec.OutputLines(cmd)
if err != nil {
return "", errors.Wrap(err, "failed to get api server port")
}
if len(lines) == 1 && lines[0] != "" {
return lines[0], nil
}
// else, retrieve the specific port mapping via NetworkSettings.Ports
cmd = exec.Command(
p.Binary(), "inspect",
"--format", fmt.Sprintf(
"{{ with (index (index .NetworkSettings.Ports \"%d/tcp\") 0) }}{{ printf \"%%s\t%%s\" .HostIp .HostPort }}{{ end }}", common.APIServerInternalPort,
),
n.String(),
)
lines, err = exec.OutputLines(cmd)
if err != nil {
return "", errors.Wrap(err, "failed to get api server port")
}
if len(lines) != 1 {
return "", errors.Errorf("network details should only be one line, got %d lines", len(lines))
}
parts := strings.Split(lines[0], "\t")
if len(parts) != 2 {
return "", errors.Errorf("network details should only be two parts, got %d", len(parts))
}
// join host and port
return net.JoinHostPort(parts[0], parts[1]), nil
}
// GetAPIServerInternalEndpoint is part of the providers.Provider interface
func (p *provider) GetAPIServerInternalEndpoint(cluster string) (string, error) {
// locate the node that hosts this
allNodes, err := p.ListNodes(cluster)
if err != nil {
return "", errors.Wrap(err, "failed to list nodes")
}
n, err := nodeutils.APIServerEndpointNode(allNodes)
if err != nil {
return "", errors.Wrap(err, "failed to get api server endpoint")
}
// NOTE: we're using the nodes's hostnames which are their names
return net.JoinHostPort(n.String(), fmt.Sprintf("%d", common.APIServerInternalPort)), nil
}
// node returns a new node handle for this provider
func (p *provider) node(name string) nodes.Node {
return &node{
binaryName: p.binaryName,
name: name,
}
}
// CollectLogs will populate dir with cluster logs and other debug files
func (p *provider) CollectLogs(dir string, nodes []nodes.Node) error {
execToPathFn := func(cmd exec.Cmd, path string) func() error {
return func() error {
f, err := common.FileOnHost(path)
if err != nil {
return err
}
defer f.Close()
return cmd.SetStdout(f).SetStderr(f).Run()
}
}
// construct a slice of methods to collect logs
fns := []func() error{
// record info about the host nerdctl
execToPathFn(
exec.Command(p.Binary(), "info"),
filepath.Join(dir, "docker-info.txt"),
),
}
// collect /var/log for each node and plan collecting more logs
var errs []error
for _, n := range nodes {
node := n // https://golang.org/doc/faq#closures_and_goroutines
name := node.String()
path := filepath.Join(dir, name)
if err := internallogs.DumpDir(p.logger, node, "/var/log", path); err != nil {
errs = append(errs, err)
}
fns = append(fns,
func() error { return common.CollectLogs(node, path) },
execToPathFn(exec.Command(p.Binary(), "inspect", name), filepath.Join(path, "inspect.json")),
func() error {
f, err := common.FileOnHost(filepath.Join(path, "serial.log"))
if err != nil {
return err
}
defer f.Close()
return node.SerialLogs(f)
},
)
}
// run and collect up all errors
errs = append(errs, errors.AggregateConcurrent(fns))
return errors.NewAggregate(errs)
}
// Info returns the provider info.
// The info is cached on the first time of the execution.
func (p *provider) Info() (*providers.ProviderInfo, error) {
var err error
if p.info == nil {
p.info, err = info(p.Binary())
}
return p.info, err
}
// dockerInfo corresponds to `docker info --format '{{json .}}'`
type dockerInfo struct {
CgroupDriver string `json:"CgroupDriver"` // "systemd", "cgroupfs", "none"
CgroupVersion string `json:"CgroupVersion"` // e.g. "2"
MemoryLimit bool `json:"MemoryLimit"`
PidsLimit bool `json:"PidsLimit"`
CPUShares bool `json:"CPUShares"`
SecurityOptions []string `json:"SecurityOptions"`
}
func info(binaryName string) (*providers.ProviderInfo, error) {
cmd := exec.Command(binaryName, "info", "--format", "{{json .}}")
out, err := exec.Output(cmd)
if err != nil {
return nil, errors.Wrap(err, "failed to get nerdctl info")
}
var dInfo dockerInfo
if err := json.Unmarshal(out, &dInfo); err != nil {
return nil, err
}
info := providers.ProviderInfo{
Cgroup2: dInfo.CgroupVersion == "2",
}
// When CgroupDriver == "none", the MemoryLimit/PidsLimit/CPUShares
// values are meaningless and need to be considered false.
// https://github.com/moby/moby/issues/42151
if dInfo.CgroupDriver != "none" {
info.SupportsMemoryLimit = dInfo.MemoryLimit
info.SupportsPidsLimit = dInfo.PidsLimit
info.SupportsCPUShares = dInfo.CPUShares
}
for _, o := range dInfo.SecurityOptions {
// o is like "name=seccomp,profile=default", or "name=rootless",
csvReader := csv.NewReader(strings.NewReader(o))
sliceSlice, err := csvReader.ReadAll()
if err != nil {
return nil, err
}
for _, f := range sliceSlice {
for _, ff := range f {
if ff == "name=rootless" {
info.Rootless = true
}
}
}
}
return &info, nil
}

View File

@ -0,0 +1,388 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package nerdctl
import (
"context"
"fmt"
"net"
"path/filepath"
"strings"
"time"
"sigs.k8s.io/kind/pkg/cluster/constants"
"sigs.k8s.io/kind/pkg/errors"
"sigs.k8s.io/kind/pkg/exec"
"sigs.k8s.io/kind/pkg/fs"
"sigs.k8s.io/kind/pkg/cluster/internal/loadbalancer"
"sigs.k8s.io/kind/pkg/cluster/internal/providers/common"
"sigs.k8s.io/kind/pkg/internal/apis/config"
)
// planCreation creates a slice of funcs that will create the containers
func planCreation(cfg *config.Cluster, networkName, binaryName string) (createContainerFuncs []func() error, err error) {
// we need to know all the names for NO_PROXY
// compute the names first before any actual node details
nodeNamer := common.MakeNodeNamer(cfg.Name)
names := make([]string, len(cfg.Nodes))
for i, node := range cfg.Nodes {
name := nodeNamer(string(node.Role)) // name the node
names[i] = name
}
haveLoadbalancer := config.ClusterHasImplicitLoadBalancer(cfg)
if haveLoadbalancer {
names = append(names, nodeNamer(constants.ExternalLoadBalancerNodeRoleValue))
}
// these apply to all container creation
genericArgs, err := commonArgs(cfg.Name, cfg, networkName, names, binaryName)
if err != nil {
return nil, err
}
// only the external LB should reflect the port if we have multiple control planes
apiServerPort := cfg.Networking.APIServerPort
apiServerAddress := cfg.Networking.APIServerAddress
if haveLoadbalancer {
// TODO: picking ports locally is less than ideal with remote docker
// but this is supposed to be an implementation detail and NOT picking
// them breaks host reboot ...
// For now remote docker + multi control plane is not supported
apiServerPort = 0 // replaced with random ports
apiServerAddress = "127.0.0.1" // only the LB needs to be non-local
// only for IPv6 only clusters
if cfg.Networking.IPFamily == config.IPv6Family {
apiServerAddress = "::1" // only the LB needs to be non-local
}
// plan loadbalancer node
name := names[len(names)-1]
createContainerFuncs = append(createContainerFuncs, func() error {
args, err := runArgsForLoadBalancer(cfg, name, genericArgs)
if err != nil {
return err
}
return createContainer(name, args, binaryName)
})
}
// plan normal nodes
for i, node := range cfg.Nodes {
node := node.DeepCopy() // copy so we can modify
name := names[i]
// fixup relative paths, docker can only handle absolute paths
for m := range node.ExtraMounts {
hostPath := node.ExtraMounts[m].HostPath
if !fs.IsAbs(hostPath) {
absHostPath, err := filepath.Abs(hostPath)
if err != nil {
return nil, errors.Wrapf(err, "unable to resolve absolute path for hostPath: %q", hostPath)
}
node.ExtraMounts[m].HostPath = absHostPath
}
}
// plan actual creation based on role
switch node.Role {
case config.ControlPlaneRole:
createContainerFuncs = append(createContainerFuncs, func() error {
node.ExtraPortMappings = append(node.ExtraPortMappings,
config.PortMapping{
ListenAddress: apiServerAddress,
HostPort: apiServerPort,
ContainerPort: common.APIServerInternalPort,
},
)
args, err := runArgsForNode(node, cfg.Networking.IPFamily, name, genericArgs)
if err != nil {
return err
}
return createContainerWithWaitUntilSystemdReachesMultiUserSystem(name, args, binaryName)
})
case config.WorkerRole:
createContainerFuncs = append(createContainerFuncs, func() error {
args, err := runArgsForNode(node, cfg.Networking.IPFamily, name, genericArgs)
if err != nil {
return err
}
return createContainerWithWaitUntilSystemdReachesMultiUserSystem(name, args, binaryName)
})
default:
return nil, errors.Errorf("unknown node role: %q", node.Role)
}
}
return createContainerFuncs, nil
}
// commonArgs computes static arguments that apply to all containers
func commonArgs(cluster string, cfg *config.Cluster, networkName string, nodeNames []string, binaryName string) ([]string, error) {
// standard arguments all nodes containers need, computed once
args := []string{
"--detach", // run the container detached
"--tty", // allocate a tty for entrypoint logs
// label the node with the cluster ID
"--label", fmt.Sprintf("%s=%s", clusterLabelKey, cluster),
// user a user defined network so we get embedded DNS
"--net", networkName,
// containerd supports the following restart modes:
// - no
// - on-failure[:max-retries]
// - unless-stopped
// - always
//
// What we desire is:
// - restart on host / container runtime reboot
// - don't restart for any other reason
//
"--restart=on-failure:1",
// this can be enabled by default in docker daemon.json, so we explicitly
// disable it, we want our entrypoint to be PID1, not docker-init / tini
"--init=false",
}
// enable IPv6 if necessary
if config.ClusterHasIPv6(cfg) {
args = append(args, "--sysctl=net.ipv6.conf.all.disable_ipv6=0", "--sysctl=net.ipv6.conf.all.forwarding=1")
}
// pass proxy environment variables
proxyEnv, err := getProxyEnv(cfg, networkName, nodeNames, binaryName)
if err != nil {
return nil, errors.Wrap(err, "proxy setup error")
}
for key, val := range proxyEnv {
args = append(args, "-e", fmt.Sprintf("%s=%s", key, val))
}
// enable /dev/fuse explicitly for fuse-overlayfs
// (Rootless Docker does not automatically mount /dev/fuse with --privileged)
if mountFuse(binaryName) {
args = append(args, "--device", "/dev/fuse")
}
if cfg.Networking.DNSSearch != nil {
args = append(args, "-e", "KIND_DNS_SEARCH="+strings.Join(*cfg.Networking.DNSSearch, " "))
}
return args, nil
}
func runArgsForNode(node *config.Node, clusterIPFamily config.ClusterIPFamily, name string, args []string) ([]string, error) {
args = append([]string{
"--hostname", name, // make hostname match container name
// label the node with the role ID
"--label", fmt.Sprintf("%s=%s", nodeRoleLabelKey, node.Role),
// running containers in a container requires privileged
// NOTE: we could try to replicate this with --cap-add, and use less
// privileges, but this flag also changes some mounts that are necessary
// including some ones docker would otherwise do by default.
// for now this is what we want. in the future we may revisit this.
"--privileged",
"--security-opt", "seccomp=unconfined", // also ignore seccomp
"--security-opt", "apparmor=unconfined", // also ignore apparmor
// runtime temporary storage
"--tmpfs", "/tmp", // various things depend on working /tmp
"--tmpfs", "/run", // systemd wants a writable /run
// runtime persistent storage
// this ensures that E.G. pods, logs etc. are not on the container
// filesystem, which is not only better for performance, but allows
// running kind in kind for "party tricks"
// (please don't depend on doing this though!)
"--volume", "/var",
// some k8s things want to read /lib/modules
"--volume", "/lib/modules:/lib/modules:ro",
// propagate KIND_EXPERIMENTAL_CONTAINERD_SNAPSHOTTER to the entrypoint script
"-e", "KIND_EXPERIMENTAL_CONTAINERD_SNAPSHOTTER",
},
args...,
)
// convert mounts and port mappings to container run args
args = append(args, generateMountBindings(node.ExtraMounts...)...)
mappingArgs, err := generatePortMappings(clusterIPFamily, node.ExtraPortMappings...)
if err != nil {
return nil, err
}
args = append(args, mappingArgs...)
switch node.Role {
case config.ControlPlaneRole:
args = append(args, "-e", "KUBECONFIG=/etc/kubernetes/admin.conf")
}
// finally, specify the image to run
return append(args, node.Image), nil
}
func runArgsForLoadBalancer(cfg *config.Cluster, name string, args []string) ([]string, error) {
args = append([]string{
"--hostname", name, // make hostname match container name
// label the node with the role ID
"--label", fmt.Sprintf("%s=%s", nodeRoleLabelKey, constants.ExternalLoadBalancerNodeRoleValue),
},
args...,
)
// load balancer port mapping
mappingArgs, err := generatePortMappings(cfg.Networking.IPFamily,
config.PortMapping{
ListenAddress: cfg.Networking.APIServerAddress,
HostPort: cfg.Networking.APIServerPort,
ContainerPort: common.APIServerInternalPort,
},
)
if err != nil {
return nil, err
}
args = append(args, mappingArgs...)
// finally, specify the image to run
return append(args, loadbalancer.Image), nil
}
func getProxyEnv(cfg *config.Cluster, networkName string, nodeNames []string, binaryName string) (map[string]string, error) {
envs := common.GetProxyEnvs(cfg)
// Specifically add the docker network subnets to NO_PROXY if we are using a proxy
if len(envs) > 0 {
subnets, err := getSubnets(networkName, binaryName)
if err != nil {
return nil, err
}
noProxyList := append(subnets, envs[common.NOProxy])
noProxyList = append(noProxyList, nodeNames...)
// Add pod and service dns names to no_proxy to allow in cluster
// Note: this is best effort based on the default CoreDNS spec
// https://github.com/kubernetes/dns/blob/master/docs/specification.md
// Any user created pod/service hostnames, namespaces, custom DNS services
// are expected to be no-proxied by the user explicitly.
noProxyList = append(noProxyList, ".svc", ".svc.cluster", ".svc.cluster.local")
noProxyJoined := strings.Join(noProxyList, ",")
envs[common.NOProxy] = noProxyJoined
envs[strings.ToLower(common.NOProxy)] = noProxyJoined
}
return envs, nil
}
func getSubnets(networkName, binaryName string) ([]string, error) {
format := `{{range (index (index . "IPAM") "Config")}}{{index . "Subnet"}} {{end}}`
cmd := exec.Command(binaryName, "network", "inspect", "-f", format, networkName)
lines, err := exec.OutputLines(cmd)
if err != nil {
return nil, errors.Wrap(err, "failed to get subnets")
}
return strings.Split(strings.TrimSpace(lines[0]), " "), nil
}
// generateMountBindings converts the mount list to a list of args for docker
// '<HostPath>:<ContainerPath>[:options]', where 'options'
// is a comma-separated list of the following strings:
// 'ro', if the path is read only
// 'Z', if the volume requires SELinux relabeling
func generateMountBindings(mounts ...config.Mount) []string {
args := make([]string, 0, len(mounts))
for _, m := range mounts {
bind := fmt.Sprintf("%s:%s", m.HostPath, m.ContainerPath)
var attrs []string
if m.Readonly {
attrs = append(attrs, "ro")
}
// Only request relabeling if the pod provides an SELinux context. If the pod
// does not provide an SELinux context relabeling will label the volume with
// the container's randomly allocated MCS label. This would restrict access
// to the volume to the container which mounts it first.
if m.SelinuxRelabel {
attrs = append(attrs, "Z")
}
switch m.Propagation {
case config.MountPropagationNone:
// noop, private is default
case config.MountPropagationBidirectional:
attrs = append(attrs, "rshared")
case config.MountPropagationHostToContainer:
attrs = append(attrs, "rslave")
default: // Falls back to "private"
}
if len(attrs) > 0 {
bind = fmt.Sprintf("%s:%s", bind, strings.Join(attrs, ","))
}
args = append(args, fmt.Sprintf("--volume=%s", bind))
}
return args
}
// generatePortMappings converts the portMappings list to a list of args for docker
func generatePortMappings(clusterIPFamily config.ClusterIPFamily, portMappings ...config.PortMapping) ([]string, error) {
args := make([]string, 0, len(portMappings))
for _, pm := range portMappings {
// do provider internal defaulting
// in a future API revision we will handle this at the API level and remove this
if pm.ListenAddress == "" {
switch clusterIPFamily {
case config.IPv4Family, config.DualStackFamily:
pm.ListenAddress = "0.0.0.0" // this is the docker default anyhow
case config.IPv6Family:
pm.ListenAddress = "::"
default:
return nil, errors.Errorf("unknown cluster IP family: %v", clusterIPFamily)
}
}
if string(pm.Protocol) == "" {
pm.Protocol = config.PortMappingProtocolTCP // TCP is the default
}
// validate that the provider can handle this binding
switch pm.Protocol {
case config.PortMappingProtocolTCP:
case config.PortMappingProtocolUDP:
case config.PortMappingProtocolSCTP:
default:
return nil, errors.Errorf("unknown port mapping protocol: %v", pm.Protocol)
}
// get a random port if necessary (port = 0)
hostPort, releaseHostPortFn, err := common.PortOrGetFreePort(pm.HostPort, pm.ListenAddress)
if err != nil {
return nil, errors.Wrap(err, "failed to get random host port for port mapping")
}
if releaseHostPortFn != nil {
defer releaseHostPortFn()
}
// generate the actual mapping arg
protocol := string(pm.Protocol)
hostPortBinding := net.JoinHostPort(pm.ListenAddress, fmt.Sprintf("%d", hostPort))
args = append(args, fmt.Sprintf("--publish=%s:%d/%s", hostPortBinding, pm.ContainerPort, protocol))
}
return args, nil
}
func createContainer(name string, args []string, binaryName string) error {
return exec.Command(binaryName, append([]string{"run", "--name", name}, args...)...).Run()
}
func createContainerWithWaitUntilSystemdReachesMultiUserSystem(name string, args []string, binaryName string) error {
if err := exec.Command(binaryName, append([]string{"run", "--name", name}, args...)...).Run(); err != nil {
return err
}
logCtx, logCancel := context.WithTimeout(context.Background(), 30*time.Second)
logCmd := exec.CommandContext(logCtx, binaryName, "logs", "-f", name)
defer logCancel()
return common.WaitUntilLogRegexpMatches(logCtx, logCmd, common.NodeReachedCgroupsReadyRegexp())
}

View File

@ -0,0 +1,52 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package nerdctl
import (
"strings"
"sigs.k8s.io/kind/pkg/exec"
)
// IsAvailable checks if nerdctl (or finch) is available in the system
func IsAvailable() bool {
cmd := exec.Command("nerdctl", "-v")
lines, err := exec.OutputLines(cmd)
if err != nil || len(lines) != 1 {
// check finch
cmd = exec.Command("finch", "-v")
lines, err = exec.OutputLines(cmd)
if err != nil || len(lines) != 1 {
return false
}
return strings.HasPrefix(lines[0], "finch version")
}
return strings.HasPrefix(lines[0], "nerdctl version")
}
// rootless: use fuse-overlayfs by default
// https://github.com/kubernetes-sigs/kind/issues/2275
func mountFuse(binaryName string) bool {
i, err := info(binaryName)
if err != nil {
return false
}
if i != nil && i.Rootless {
return true
}
return false
}

View File

@ -421,10 +421,7 @@ func generatePortMappings(clusterIPFamily config.ClusterIPFamily, portMappings .
} }
func createContainer(name string, args []string) error { func createContainer(name string, args []string) error {
if err := exec.Command("podman", append([]string{"run", "--name", name}, args...)...).Run(); err != nil { return exec.Command("podman", append([]string{"run", "--name", name}, args...)...).Run()
return err
}
return nil
} }
func createContainerWithWaitUntilSystemdReachesMultiUserSystem(name string, args []string) error { func createContainerWithWaitUntilSystemdReachesMultiUserSystem(name string, args []string) error {

View File

@ -34,6 +34,7 @@ import (
"sigs.k8s.io/kind/pkg/cluster/internal/kubeconfig" "sigs.k8s.io/kind/pkg/cluster/internal/kubeconfig"
internalproviders "sigs.k8s.io/kind/pkg/cluster/internal/providers" internalproviders "sigs.k8s.io/kind/pkg/cluster/internal/providers"
"sigs.k8s.io/kind/pkg/cluster/internal/providers/docker" "sigs.k8s.io/kind/pkg/cluster/internal/providers/docker"
"sigs.k8s.io/kind/pkg/cluster/internal/providers/nerdctl"
"sigs.k8s.io/kind/pkg/cluster/internal/providers/podman" "sigs.k8s.io/kind/pkg/cluster/internal/providers/podman"
) )
@ -102,8 +103,8 @@ var NoNodeProviderDetectedError = errors.NewWithoutStack("failed to detect any s
// Pass the returned ProviderOption to NewProvider to pass the auto-detect Docker // Pass the returned ProviderOption to NewProvider to pass the auto-detect Docker
// or Podman option explicitly (in the future there will be more options) // or Podman option explicitly (in the future there will be more options)
// //
// NOTE: The kind *cli* also checks `KIND_EXPERIMENTAL_PROVIDER` for "podman" or // NOTE: The kind *cli* also checks `KIND_EXPERIMENTAL_PROVIDER` for "podman",
// "docker" currently and does not auto-detect / respects this if set. // "nerctl" or "docker" currently and does not auto-detect / respects this if set.
// //
// This will be replaced with some other mechanism in the future (likely when // This will be replaced with some other mechanism in the future (likely when
// podman support is GA), in the meantime though your tool may wish to match this. // podman support is GA), in the meantime though your tool may wish to match this.
@ -115,6 +116,9 @@ func DetectNodeProvider() (ProviderOption, error) {
if docker.IsAvailable() { if docker.IsAvailable() {
return ProviderWithDocker(), nil return ProviderWithDocker(), nil
} }
if nerdctl.IsAvailable() {
return ProviderWithNerdctl(""), nil
}
if podman.IsAvailable() { if podman.IsAvailable() {
return ProviderWithPodman(), nil return ProviderWithPodman(), nil
} }
@ -167,6 +171,13 @@ func ProviderWithPodman() ProviderOption {
}) })
} }
// ProviderWithNerdctl configures the provider to use the nerdctl runtime
func ProviderWithNerdctl(binaryName string) ProviderOption {
return providerRuntimeOption(func(p *Provider) {
p.provider = nerdctl.NewProvider(p.logger, binaryName)
})
}
// Create provisions and starts a kubernetes-in-docker cluster // Create provisions and starts a kubernetes-in-docker cluster
func (p *Provider) Create(name string, options ...CreateOption) error { func (p *Provider) Create(name string, options ...CreateOption) error {
// apply options // apply options

View File

@ -54,11 +54,11 @@ func DisplayVersion() string {
} }
// versionCore is the core portion of the kind CLI version per Semantic Versioning 2.0.0 // versionCore is the core portion of the kind CLI version per Semantic Versioning 2.0.0
const versionCore = "0.22.0" const versionCore = "0.24.0"
// versionPreRelease is the base pre-release portion of the kind CLI version per // versionPreRelease is the base pre-release portion of the kind CLI version per
// Semantic Versioning 2.0.0 // Semantic Versioning 2.0.0
const versionPreRelease = "" var versionPreRelease = ""
// gitCommitCount count the commits since the last release. // gitCommitCount count the commits since the last release.
// It is injected at build time. // It is injected at build time.

View File

@ -64,10 +64,10 @@ func Copy(src, dst string) error {
return err return err
} }
// do real copy work // do real copy work
return copy(src, dst, info) return copyWithSrcInfo(src, dst, info)
} }
func copy(src, dst string, info os.FileInfo) error { func copyWithSrcInfo(src, dst string, info os.FileInfo) error {
if info.Mode()&os.ModeSymlink != 0 { if info.Mode()&os.ModeSymlink != 0 {
return copySymlink(src, dst) return copySymlink(src, dst)
} }
@ -128,7 +128,7 @@ func copySymlink(src, dst string) error {
return err return err
} }
// copy the underlying contents // copy the underlying contents
return copy(realSrc, dst, info) return copyWithSrcInfo(realSrc, dst, info)
} }
func copyDir(src, dst string, info os.FileInfo) error { func copyDir(src, dst string, info os.FileInfo) error {
@ -148,7 +148,7 @@ func copyDir(src, dst string, info os.FileInfo) error {
if err != nil { if err != nil {
return err return err
} }
if err := copy(entrySrc, entryDst, fileInfo); err != nil { if err := copyWithSrcInfo(entrySrc, entryDst, fileInfo); err != nil {
return err return err
} }
} }

View File

@ -148,7 +148,7 @@ type Networking struct {
// If DisableDefaultCNI is true, kind will not install the default CNI setup. // If DisableDefaultCNI is true, kind will not install the default CNI setup.
// Instead the user should install their own CNI after creating the cluster. // Instead the user should install their own CNI after creating the cluster.
DisableDefaultCNI bool DisableDefaultCNI bool
// KubeProxyMode defines if kube-proxy should operate in iptables or ipvs mode // KubeProxyMode defines if kube-proxy should operate in iptables, ipvs or nftables mode
KubeProxyMode ProxyMode KubeProxyMode ProxyMode
// DNSSearch defines the DNS search domain to use for nodes. If not set, this will be inherited from the host. // DNSSearch defines the DNS search domain to use for nodes. If not set, this will be inherited from the host.
DNSSearch *[]string DNSSearch *[]string
@ -174,6 +174,8 @@ const (
IPTablesProxyMode ProxyMode = "iptables" IPTablesProxyMode ProxyMode = "iptables"
// IPVSProxyMode sets ProxyMode to ipvs // IPVSProxyMode sets ProxyMode to ipvs
IPVSProxyMode ProxyMode = "ipvs" IPVSProxyMode ProxyMode = "ipvs"
// NFTablesProxyMode sets ProxyMode to nftables
NFTablesProxyMode ProxyMode = "nftables"
// NoneProxyMode disables kube-proxy // NoneProxyMode disables kube-proxy
NoneProxyMode ProxyMode = "none" NoneProxyMode ProxyMode = "none"
) )

View File

@ -52,6 +52,11 @@ func (c *Cluster) Validate() error {
} }
} }
// ipFamily should be ipv4, ipv6, or dual
if c.Networking.IPFamily != IPv4Family && c.Networking.IPFamily != IPv6Family && c.Networking.IPFamily != DualStackFamily {
errs = append(errs, errors.Errorf("invalid ipFamily: %s", c.Networking.IPFamily))
}
// podSubnet should be a valid CIDR // podSubnet should be a valid CIDR
if err := validateSubnets(c.Networking.PodSubnet, c.Networking.IPFamily); err != nil { if err := validateSubnets(c.Networking.PodSubnet, c.Networking.IPFamily); err != nil {
errs = append(errs, errors.Errorf("invalid pod subnet %v", err)) errs = append(errs, errors.Errorf("invalid pod subnet %v", err))
@ -64,7 +69,7 @@ func (c *Cluster) Validate() error {
// KubeProxyMode should be iptables or ipvs // KubeProxyMode should be iptables or ipvs
if c.Networking.KubeProxyMode != IPTablesProxyMode && c.Networking.KubeProxyMode != IPVSProxyMode && if c.Networking.KubeProxyMode != IPTablesProxyMode && c.Networking.KubeProxyMode != IPVSProxyMode &&
c.Networking.KubeProxyMode != NoneProxyMode { c.Networking.KubeProxyMode != NoneProxyMode && c.Networking.KubeProxyMode != NFTablesProxyMode {
errs = append(errs, errors.Errorf("invalid kubeProxyMode: %s", c.Networking.KubeProxyMode)) errs = append(errs, errors.Errorf("invalid kubeProxyMode: %s", c.Networking.KubeProxyMode))
} }