From ed8d51a95e99c8bf0523d3a1b499edf0ea4fbcf5 Mon Sep 17 00:00:00 2001 From: Alonza0314 Date: Fri, 17 Apr 2026 02:58:44 +0000 Subject: [PATCH 1/8] feat: topo check --- cmd/error.go | 10 + cmd/topo.go | 47 +++ go.mod | 42 ++- go.sum | 123 ++++++- integration-test/topo/topo_template.yaml | 20 ++ internal/topo/apply.go | 9 + internal/topo/model.go | 23 ++ internal/topo/topo.go | 112 ++++++ internal/topo/topo_test.go | 418 +++++++++++++++++++++++ 9 files changed, 802 insertions(+), 2 deletions(-) create mode 100644 cmd/topo.go create mode 100644 integration-test/topo/topo_template.yaml create mode 100644 internal/topo/apply.go create mode 100644 internal/topo/model.go create mode 100644 internal/topo/topo.go create mode 100644 internal/topo/topo_test.go diff --git a/cmd/error.go b/cmd/error.go index 9ac85dd..6a24537 100644 --- a/cmd/error.go +++ b/cmd/error.go @@ -25,3 +25,13 @@ func errPrint(err error) { fmt.Printf("Error: %v\n", err) os.Exit(1) } + +func errFileNotExist(file string) { + fmt.Printf("File '%s' does not exist\n", file) + os.Exit(1) +} + +func errYamlFormat(file string, err error) { + fmt.Printf("Error parsing YAML file '%s': %v\n", file, err) + os.Exit(1) +} diff --git a/cmd/topo.go b/cmd/topo.go new file mode 100644 index 0000000..256351f --- /dev/null +++ b/cmd/topo.go @@ -0,0 +1,47 @@ +package cmd + +import ( + "fmt" + + "github.com/Alonza0314/nsctl/internal/topo" + "github.com/free-ran-ue/util" + "github.com/spf13/cobra" +) + +var topoCmd = &cobra.Command{ + Use: "topo [args]", + Short: "Manage the topology of namespaces and networks", + Long: "Manage the topology of namespaces and networks", + Run: topoFunc, +} + +func init() { + nsctlCmd.AddCommand(topoCmd) +} + +func topoFunc(cmd *cobra.Command, args []string) { + if len(args) == 0 { + errEmptyAction() + } + + if !util.FileExists(args[1]) { + errFileNotExist(args[1]) + } + + var topoStruct topo.Topology + if err := util.LoadFromYaml(args[1], &topoStruct); err != nil { + errYamlFormat(args[1], err) + } + + switch args[0] { + case "apply": + if err := topo.Apply(&topoStruct); err != nil { + errPrint(err) + } else { + fmt.Printf("Topology applied successfully from file: %s\n", args[1]) + } + case "delete": + default: + errFormat(args) + } +} diff --git a/go.mod b/go.mod index e3e1e9e..34d4cf7 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,53 @@ module github.com/Alonza0314/nsctl go 1.25.5 require ( + github.com/free-ran-ue/util v0.0.3 github.com/spf13/cobra v1.10.2 github.com/vishvananda/netlink v1.3.1 github.com/vishvananda/netns v0.0.5 ) require ( + github.com/Alonza0314/logger-go/v2 v2.0.5 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/free-ran-ue/free-ran-ue/v2 v2.3.11 // indirect + github.com/free5gc/aper v1.1.0 // indirect + github.com/free5gc/nas v1.2.2 // indirect + github.com/free5gc/ngap v1.1.2 // indirect + github.com/free5gc/openapi v1.2.3 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/gin-gonic/gin v1.12.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.9 // indirect - golang.org/x/sys v0.10.0 // indirect + github.com/tim-ywliu/nested-logrus-formatter v1.3.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect + golang.org/x/arch v0.22.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 28cbdad..59036a4 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,138 @@ +github.com/Alonza0314/logger-go/v2 v2.0.5 h1:ZCOHbPEZ4tXrS7PC3ujOKOwILKeRC3sOW1/1ItLECoo= +github.com/Alonza0314/logger-go/v2 v2.0.5/go.mod h1:fRFP+N39GOCRbuYMDc5PI9KJvzTyxpGNhpyHkIhIsoY= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/free-ran-ue/free-ran-ue/v2 v2.3.11 h1:FLW1ek4AjJFMGSHlyW/9uzWwoFDu7c24Wvbidu18jT4= +github.com/free-ran-ue/free-ran-ue/v2 v2.3.11/go.mod h1:JCi9foIclBHMXktHFMC7GrEso1n8MRzv5J1qYxnsEqQ= +github.com/free-ran-ue/util v0.0.3 h1:xGiSfyjuYuOFyvp0n2gVDTLICAJHIbicGtYhks/1Jik= +github.com/free-ran-ue/util v0.0.3/go.mod h1:qTBid7HFU0BIbYyfwNrBiwU6mjDwrim1Rl+2QnjYuqQ= +github.com/free5gc/aper v1.1.0 h1:X36hts0PYQuN3d+VXpYsUZaibrokP8nbBVIQBVY2bNI= +github.com/free5gc/aper v1.1.0/go.mod h1:v7xVwA5XaD9lBnZDOzvDF0Nw6GOcn32IrL7tM6zHPyM= +github.com/free5gc/nas v1.2.2 h1:r2+9CHY1TRfTM/X2Pc+vBizeqweUJRuLo1DGub1vk5s= +github.com/free5gc/nas v1.2.2/go.mod h1:y3f4YCGsvQOqQx8h+4MH22k0C8XaopK2ZP/F7qAW98k= +github.com/free5gc/ngap v1.1.2 h1:AA+s9+eaE8fSTnmBbh1xiD3VPaszonCd7AQJGKX5PHM= +github.com/free5gc/ngap v1.1.2/go.mod h1:4HYtB+msoTBc3cEPlSr4ZUUHn3BFZhdPuVASERMzVeI= +github.com/free5gc/openapi v1.2.3 h1:w4TmYBR8TUE4ZgKo7eiMZbyZPURSBFlMWiFeX+OsiA8= +github.com/free5gc/openapi v1.2.3/go.mod h1:fLvaBtUZrvrzkKrmn5Aza+JNbpWnp3kxKixu6kLSD3k= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/go-playground/assert v1.2.1 h1:ad06XqC+TOv0nJWnbULSlh3ehp5uLuQEojZY5Tq8RgI= +github.com/go-playground/assert v1.2.1/go.mod h1:Lgy+k19nOB/wQG/fVSQ7rra5qYugmytMQqvQ2dgjWn8= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tim-ywliu/nested-logrus-formatter v1.3.2 h1:jugNJ2/CNCI79SxOJCOhwUHeN3O7/7/bj+ZRGOFlCSw= +github.com/tim-ywliu/nested-logrus-formatter v1.3.2/go.mod h1:oGPmcxZB65j9Wo7mCnQKSrKEJtVDqyjD666SGmyStXI= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= +golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/integration-test/topo/topo_template.yaml b/integration-test/topo/topo_template.yaml new file mode 100644 index 0000000..96eea65 --- /dev/null +++ b/integration-test/topo/topo_template.yaml @@ -0,0 +1,20 @@ +namespaces: + - name: test-1 + commands: + - ls + networks: + - name: br-test-1 + bridge: br-test-1-2 + ipv4: 10.0.0.1 + + - name: test-2 + commands: + - ls + networks: + - name: br-test-2 + bridge: br-test-1-2 + ipv4: 10.0.0.2 + +networks: + - name: br-test-1-2 + subnet: 10.0.0.0/24 \ No newline at end of file diff --git a/internal/topo/apply.go b/internal/topo/apply.go new file mode 100644 index 0000000..f0d7c8b --- /dev/null +++ b/internal/topo/apply.go @@ -0,0 +1,9 @@ +package topo + +func Apply(topo *Topology) error { + if err := checkTopo(topo); err != nil { + return err + } + + return nil +} \ No newline at end of file diff --git a/internal/topo/model.go b/internal/topo/model.go new file mode 100644 index 0000000..829b0a8 --- /dev/null +++ b/internal/topo/model.go @@ -0,0 +1,23 @@ +package topo + +type Topology struct { + Namespaces []Namespace `yaml:"namespaces" validate:"required"` + Networks []Network `yaml:"networks" validate:"required"` +} + +type Namespace struct { + Name string `yaml:"name" validate:"required"` + Commands []string `yaml:"commands"` + Networks []Net `yaml:"networks"` +} + +type Network struct { + Name string `yaml:"name" validate:"required"` + Subnet string `yaml:"subnet" validate:"required,ipv4cidr"` +} + +type Net struct { + Name string `yaml:"name" validate:"required"` + Bridge string `yaml:"bridge" validate:"required"` + Ipv4 string `yaml:"ipv4" validate:"required,ipv4cidr"` +} diff --git a/internal/topo/topo.go b/internal/topo/topo.go new file mode 100644 index 0000000..ef1dfe7 --- /dev/null +++ b/internal/topo/topo.go @@ -0,0 +1,112 @@ +package topo + +import ( + "fmt" + "net" +) + +func checkTopo(topo *Topology) error { + if err := checkTopoNamespace(topo); err != nil { + return err + } + + if err := checkTopoNetwork(topo); err != nil { + return err + } + + return nil +} + +func checkTopoNamespace(topo *Topology) error { + nsNames := make(map[string]struct{}) + + for _, ns := range topo.Namespaces { + if _, exists := nsNames[ns.Name]; exists { + return fmt.Errorf("duplicate namespace name: %s", ns.Name) + } + nsNames[ns.Name] = struct{}{} + + if err := checkNamespaceNetwork(&ns); err != nil { + return err + } + + } + + return nil +} + +func checkNamespaceNetwork(ns *Namespace) error { + netNames, ips := make(map[string]struct{}), make(map[string]struct{}) + + for _, network := range ns.Networks { + if _, exists := netNames[network.Name]; exists { + return fmt.Errorf("duplicate network name in namespace %s: %s", ns.Name, network.Name) + } + netNames[network.Name] = struct{}{} + + if _, exists := ips[network.Ipv4]; exists { + return fmt.Errorf("duplicate IP address in namespace %s: %s", ns.Name, network.Ipv4) + } + ips[network.Ipv4] = struct{}{} + } + + return nil +} + +func checkTopoNetwork(topo *Topology) error { + bridges, subnets, bridgeToSubnet := make(map[string][]Net), make(map[string]struct{}), make(map[string]*net.IPNet) + + for _, network := range topo.Networks { + if _, exists := bridges[network.Name]; exists { + return fmt.Errorf("duplicate network name: %s", network.Name) + } + + _, subnet, err := net.ParseCIDR(network.Subnet) + if err != nil { + return fmt.Errorf("invalid subnet CIDR for network %s: %s", network.Name, network.Subnet) + } + + if _, exists := subnets[subnet.String()]; exists { + return fmt.Errorf("duplicate subnet: %s", network.Subnet) + } + + bridges[network.Name], subnets[subnet.String()], bridgeToSubnet[network.Name] = make([]Net, 0), struct{}{}, subnet + } + + for _, ns := range topo.Namespaces { + for _, network := range ns.Networks { + if _, exists := bridges[network.Bridge]; !exists { + return fmt.Errorf("network bridge %s in namespace %s is not defined in topo networks", network.Bridge, ns.Name) + } + bridges[network.Bridge] = append(bridges[network.Bridge], network) + } + } + + for bdg, nets := range bridges { + ips := make(map[string]struct{}) + for _, ip := range nets { + if _, exists := ips[ip.Ipv4]; exists { + return fmt.Errorf("duplicate IP address %s for bridge %s", ip.Ipv4, bdg) + } + if err := checkSubnet(bridgeToSubnet[bdg], ip.Ipv4); err != nil { + return fmt.Errorf("invalid IP address %s for bridge %s: %v", ip.Ipv4, bdg, err) + } + ips[ip.Ipv4] = struct{}{} + } + } + + return nil +} + +func checkSubnet(subnet *net.IPNet, targetIp string) error { + ip := net.ParseIP(targetIp) + if ip == nil { + return fmt.Errorf("invalid IP address: %s", targetIp) + } + + if !subnet.Contains(ip) { + return fmt.Errorf("IPv4 address %s is not in subnet %s", targetIp, subnet) + } + + return nil +} diff --git a/internal/topo/topo_test.go b/internal/topo/topo_test.go new file mode 100644 index 0000000..31d8903 --- /dev/null +++ b/internal/topo/topo_test.go @@ -0,0 +1,418 @@ +package topo + +import "testing" + +var testCheckTopoCases = []struct { + name string + topo *Topology + expectedErr bool + expectedErrDetail string +}{ + { + name: "valid topo", + topo: &Topology{ + Namespaces: []Namespace{ + { + Name: "test-1", + Commands: []string{"echo"}, + Networks: []Net{ + { + Name: "br-test-1", + Bridge: "br-test-1-2", + Ipv4: "10.0.0.1", + }, + }, + }, + { + Name: "test-2", + Commands: []string{"echo"}, + Networks: []Net{ + { + Name: "br-test-2", + Bridge: "br-test-1-2", + Ipv4: "10.0.0.2", + }, + }, + }, + }, + Networks: []Network{ + { + Name: "br-test-1-2", + Subnet: "10.0.0.0/24", + }, + }, + }, + expectedErr: false, + expectedErrDetail: "", + }, + { + name: "duplicate ns name", + topo: &Topology{ + Namespaces: []Namespace{ + { + Name: "test-1", + Commands: []string{"echo"}, + Networks: []Net{ + { + Name: "br-test-1", + Bridge: "br-test-1-2", + Ipv4: "10.0.0.1", + }, + }, + }, + { + Name: "test-1", + Commands: []string{"echo"}, + Networks: []Net{ + { + Name: "br-test-2", + Bridge: "br-test-1-2", + Ipv4: "10.0.0.2", + }, + }, + }, + }, + Networks: []Network{ + { + Name: "br-test-1-2", + Subnet: "10.0.0.0/24", + }, + }, + }, + expectedErr: true, + expectedErrDetail: "duplicate namespace name: test-1", + }, + { + name: "duplicate network name", + topo: &Topology{ + Namespaces: []Namespace{ + { + Name: "test-1", + Commands: []string{"echo"}, + Networks: []Net{ + { + Name: "br-test-1", + Bridge: "br-test-1-2", + Ipv4: "10.0.0.1", + }, + { + Name: "br-test-1", + Bridge: "br-test-1-2", + Ipv4: "10.0.0.3", + }, + }, + }, + { + Name: "test-2", + Commands: []string{"echo"}, + Networks: []Net{ + { + Name: "br-test-2", + Bridge: "br-test-1-2", + Ipv4: "10.0.0.2", + }, + }, + }, + }, + Networks: []Network{ + { + Name: "br-test-1-2", + Subnet: "10.0.0.0/24", + }, + }, + }, + expectedErr: true, + expectedErrDetail: "duplicate network name in namespace test-1: br-test-1", + }, + { + name: "duplicate IP address", + topo: &Topology{ + Namespaces: []Namespace{ + { + Name: "test-1", + Commands: []string{"echo"}, + Networks: []Net{ + { + Name: "br-test-1", + Bridge: "br-test-1-2", + Ipv4: "10.0.0.1", + }, + { + Name: "br-test-3", + Bridge: "br-test-1-2", + Ipv4: "10.0.0.1", + }, + }, + }, + { + Name: "test-2", + Commands: []string{"echo"}, + Networks: []Net{ + { + Name: "br-test-2", + Bridge: "br-test-1-2", + Ipv4: "10.0.0.2", + }, + }, + }, + }, + Networks: []Network{ + { + Name: "br-test-1-2", + Subnet: "10.0.0.0/24", + }, + }, + }, + expectedErr: true, + expectedErrDetail: "duplicate IP address in namespace test-1: 10.0.0.1", + }, + { + name: "duplicate network name", + topo: &Topology{ + Namespaces: []Namespace{ + { + Name: "test-1", + Commands: []string{"echo"}, + Networks: []Net{ + { + Name: "br-test-1", + Bridge: "br-test-1-2", + Ipv4: "10.0.0.1", + }, + }, + }, + { + Name: "test-2", + Commands: []string{"echo"}, + Networks: []Net{ + { + Name: "br-test-2", + Bridge: "br-test-1-2", + Ipv4: "10.0.0.2", + }, + }, + }, + }, + Networks: []Network{ + { + Name: "br-test-1-2", + Subnet: "10.0.0.0/24", + }, + { + Name: "br-test-1-2", + Subnet: "11.0.0.0/24", + }, + }, + }, + expectedErr: true, + expectedErrDetail: "duplicate network name: br-test-1-2", + }, + { + name: "invalid subnet CIDR", + topo: &Topology{ + Namespaces: []Namespace{ + { + Name: "test-1", + Commands: []string{"echo"}, + Networks: []Net{ + { + Name: "br-test-1", + Bridge: "br-test-1-2", + Ipv4: "10.0.0.1", + }, + }, + }, + { + Name: "test-2", + Commands: []string{"echo"}, + Networks: []Net{ + { + Name: "br-test-2", + Bridge: "br-test-1-2", + Ipv4: "10.0.0.2", + }, + }, + }, + }, + Networks: []Network{ + { + Name: "br-test-1-2", + Subnet: "10.0.0.0/100", + }, + }, + }, + expectedErr: true, + expectedErrDetail: "invalid subnet CIDR for network br-test-1-2: 10.0.0.0/100", + }, + { + name: "duplicate subnet", + topo: &Topology{ + Namespaces: []Namespace{ + { + Name: "test-1", + Commands: []string{"echo"}, + Networks: []Net{ + { + Name: "br-test-1", + Bridge: "br-test-1-2", + Ipv4: "10.0.0.1", + }, + }, + }, + { + Name: "test-2", + Commands: []string{"echo"}, + Networks: []Net{ + { + Name: "br-test-2", + Bridge: "br-test-1-2", + Ipv4: "10.0.0.2", + }, + }, + }, + }, + Networks: []Network{ + { + Name: "br-test-1-2", + Subnet: "10.0.0.0/24", + }, + { + Name: "br-test-3-4", + Subnet: "10.0.0.0/24", + }, + }, + }, + expectedErr: true, + expectedErrDetail: "duplicate subnet: 10.0.0.0/24", + }, + { + name: "duplicate IP address across namespaces", + topo: &Topology{ + Namespaces: []Namespace{ + { + Name: "test-1", + Commands: []string{"echo"}, + Networks: []Net{ + { + Name: "br-test-1", + Bridge: "br-test-1-2", + Ipv4: "10.0.0.1", + }, + }, + }, + { + Name: "test-2", + Commands: []string{"echo"}, + Networks: []Net{ + { + Name: "br-test-2", + Bridge: "br-test-1-2", + Ipv4: "10.0.0.1", + }, + }, + }, + }, + Networks: []Network{ + { + Name: "br-test-1-2", + Subnet: "10.0.0.0/24", + }, + }, + }, + expectedErr: true, + expectedErrDetail: "duplicate IP address 10.0.0.1 for bridge br-test-1-2", + }, + { + name: "invalid subnet for network bridge", + topo: &Topology{ + Namespaces: []Namespace{ + { + Name: "test-1", + Commands: []string{"echo"}, + Networks: []Net{ + { + Name: "br-test-1", + Bridge: "br-test-1-2", + Ipv4: "10.0.1.1", + }, + }, + }, + { + Name: "test-2", + Commands: []string{"echo"}, + Networks: []Net{ + { + Name: "br-test-2", + Bridge: "br-test-1-2", + Ipv4: "10.0.0.2", + }, + }, + }, + }, + Networks: []Network{ + { + Name: "br-test-1-2", + Subnet: "10.0.0.0/24", + }, + }, + }, + expectedErr: true, + expectedErrDetail: "invalid IP address 10.0.1.1 for bridge br-test-1-2: IPv4 address 10.0.1.1 is not in subnet 10.0.0.0/24", + }, + { + name: "invalid IP address", + topo: &Topology{ + Namespaces: []Namespace{ + { + Name: "test-1", + Commands: []string{"echo"}, + Networks: []Net{ + { + Name: "br-test-1", + Bridge: "br-test-1-2", + Ipv4: "10.0.0.999", + }, + }, + }, + { + Name: "test-2", + Commands: []string{"echo"}, + Networks: []Net{ + { + Name: "br-test-2", + Bridge: "br-test-1-2", + Ipv4: "10.0.0.2", + }, + }, + }, + }, + Networks: []Network{ + { + Name: "br-test-1-2", + Subnet: "10.0.0.0/24", + }, + }, + }, + expectedErr: true, + expectedErrDetail: "invalid IP address 10.0.0.999 for bridge br-test-1-2: invalid IP address: 10.0.0.999", + }, +} + +func TestCheckTopo(t *testing.T) { + for _, tc := range testCheckTopoCases { + t.Run(tc.name, func(t *testing.T) { + err := checkTopo(tc.topo) + if tc.expectedErr { + if err == nil { + t.Errorf("expected error but got nil") + } else if err.Error() != tc.expectedErrDetail { + t.Errorf("expected error detail '%s' but got '%s'", tc.expectedErrDetail, err.Error()) + } + } else { + if err != nil { + t.Errorf("expected no error but got: %v", err) + } + } + }) + } +} From 2ad6ec18dd2a57503931296d33da52f535a1c498 Mon Sep 17 00:00:00 2001 From: Alonza0314 Date: Fri, 17 Apr 2026 03:28:45 +0000 Subject: [PATCH 2/8] chore: empty line --- internal/nsExec/exec.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/nsExec/exec.go b/internal/nsExec/exec.go index eef3c9f..cc174ce 100644 --- a/internal/nsExec/exec.go +++ b/internal/nsExec/exec.go @@ -36,5 +36,6 @@ func Run(ns string, command []string) error { } } + return execErr } From a672ac685911313a8f48033fab184fa468cf90f6 Mon Sep 17 00:00:00 2001 From: Alonza0314 Date: Fri, 17 Apr 2026 03:55:48 +0000 Subject: [PATCH 3/8] chore: ip to cidr --- integration-test/topo/topo_template.yaml | 4 +- internal/topo/topo.go | 28 ++++++++++++- internal/topo/topo_test.go | 52 ++++++++++++------------ 3 files changed, 54 insertions(+), 30 deletions(-) diff --git a/integration-test/topo/topo_template.yaml b/integration-test/topo/topo_template.yaml index 96eea65..b0060db 100644 --- a/integration-test/topo/topo_template.yaml +++ b/integration-test/topo/topo_template.yaml @@ -5,7 +5,7 @@ namespaces: networks: - name: br-test-1 bridge: br-test-1-2 - ipv4: 10.0.0.1 + ipv4: 10.0.0.1/24 - name: test-2 commands: @@ -13,7 +13,7 @@ namespaces: networks: - name: br-test-2 bridge: br-test-1-2 - ipv4: 10.0.0.2 + ipv4: 10.0.0.2/24 networks: - name: br-test-1-2 diff --git a/internal/topo/topo.go b/internal/topo/topo.go index ef1dfe7..1884863 100644 --- a/internal/topo/topo.go +++ b/internal/topo/topo.go @@ -3,6 +3,8 @@ package topo import ( "fmt" "net" + + "github.com/Alonza0314/nsctl/internal/namespace" ) func checkTopo(topo *Topology) error { @@ -99,8 +101,8 @@ func checkTopoNetwork(topo *Topology) error { } func checkSubnet(subnet *net.IPNet, targetIp string) error { - ip := net.ParseIP(targetIp) - if ip == nil { + ip, _, err := net.ParseCIDR(targetIp) + if err != nil { return fmt.Errorf("invalid IP address: %s", targetIp) } @@ -110,3 +112,25 @@ func checkSubnet(subnet *net.IPNet, targetIp string) error { return nil } + +func checkExist(topo *Topology) error { + if err := checkNamespaceExist(topo.Namespaces); err != nil { + return err + } + + return nil +} + +func checkNamespaceExist(nss []Namespace) error { + for _, ns := range nss { + found, err := namespace.GetNs(namespace.GetNsName(ns.Name)) + if err != nil { + return err + } + if found { + return fmt.Errorf("namespace %s already exists", ns.Name) + } + } + + return nil +} diff --git a/internal/topo/topo_test.go b/internal/topo/topo_test.go index 31d8903..d0e6250 100644 --- a/internal/topo/topo_test.go +++ b/internal/topo/topo_test.go @@ -19,7 +19,7 @@ var testCheckTopoCases = []struct { { Name: "br-test-1", Bridge: "br-test-1-2", - Ipv4: "10.0.0.1", + Ipv4: "10.0.0.1/24", }, }, }, @@ -30,7 +30,7 @@ var testCheckTopoCases = []struct { { Name: "br-test-2", Bridge: "br-test-1-2", - Ipv4: "10.0.0.2", + Ipv4: "10.0.0.2/24", }, }, }, @@ -56,7 +56,7 @@ var testCheckTopoCases = []struct { { Name: "br-test-1", Bridge: "br-test-1-2", - Ipv4: "10.0.0.1", + Ipv4: "10.0.0.1/24", }, }, }, @@ -67,7 +67,7 @@ var testCheckTopoCases = []struct { { Name: "br-test-2", Bridge: "br-test-1-2", - Ipv4: "10.0.0.2", + Ipv4: "10.0.0.2/24", }, }, }, @@ -93,12 +93,12 @@ var testCheckTopoCases = []struct { { Name: "br-test-1", Bridge: "br-test-1-2", - Ipv4: "10.0.0.1", + Ipv4: "10.0.0.1/24", }, { Name: "br-test-1", Bridge: "br-test-1-2", - Ipv4: "10.0.0.3", + Ipv4: "10.0.0.3/24", }, }, }, @@ -109,7 +109,7 @@ var testCheckTopoCases = []struct { { Name: "br-test-2", Bridge: "br-test-1-2", - Ipv4: "10.0.0.2", + Ipv4: "10.0.0.2/24", }, }, }, @@ -135,12 +135,12 @@ var testCheckTopoCases = []struct { { Name: "br-test-1", Bridge: "br-test-1-2", - Ipv4: "10.0.0.1", + Ipv4: "10.0.0.1/24", }, { Name: "br-test-3", Bridge: "br-test-1-2", - Ipv4: "10.0.0.1", + Ipv4: "10.0.0.1/24", }, }, }, @@ -151,7 +151,7 @@ var testCheckTopoCases = []struct { { Name: "br-test-2", Bridge: "br-test-1-2", - Ipv4: "10.0.0.2", + Ipv4: "10.0.0.2/24", }, }, }, @@ -164,7 +164,7 @@ var testCheckTopoCases = []struct { }, }, expectedErr: true, - expectedErrDetail: "duplicate IP address in namespace test-1: 10.0.0.1", + expectedErrDetail: "duplicate IP address in namespace test-1: 10.0.0.1/24", }, { name: "duplicate network name", @@ -177,7 +177,7 @@ var testCheckTopoCases = []struct { { Name: "br-test-1", Bridge: "br-test-1-2", - Ipv4: "10.0.0.1", + Ipv4: "10.0.0.1/24", }, }, }, @@ -188,7 +188,7 @@ var testCheckTopoCases = []struct { { Name: "br-test-2", Bridge: "br-test-1-2", - Ipv4: "10.0.0.2", + Ipv4: "10.0.0.2/24", }, }, }, @@ -218,7 +218,7 @@ var testCheckTopoCases = []struct { { Name: "br-test-1", Bridge: "br-test-1-2", - Ipv4: "10.0.0.1", + Ipv4: "10.0.0.1/24", }, }, }, @@ -229,7 +229,7 @@ var testCheckTopoCases = []struct { { Name: "br-test-2", Bridge: "br-test-1-2", - Ipv4: "10.0.0.2", + Ipv4: "10.0.0.2/24", }, }, }, @@ -255,7 +255,7 @@ var testCheckTopoCases = []struct { { Name: "br-test-1", Bridge: "br-test-1-2", - Ipv4: "10.0.0.1", + Ipv4: "10.0.0.1/24", }, }, }, @@ -266,7 +266,7 @@ var testCheckTopoCases = []struct { { Name: "br-test-2", Bridge: "br-test-1-2", - Ipv4: "10.0.0.2", + Ipv4: "10.0.0.2/24", }, }, }, @@ -296,7 +296,7 @@ var testCheckTopoCases = []struct { { Name: "br-test-1", Bridge: "br-test-1-2", - Ipv4: "10.0.0.1", + Ipv4: "10.0.0.1/24", }, }, }, @@ -307,7 +307,7 @@ var testCheckTopoCases = []struct { { Name: "br-test-2", Bridge: "br-test-1-2", - Ipv4: "10.0.0.1", + Ipv4: "10.0.0.1/24", }, }, }, @@ -320,7 +320,7 @@ var testCheckTopoCases = []struct { }, }, expectedErr: true, - expectedErrDetail: "duplicate IP address 10.0.0.1 for bridge br-test-1-2", + expectedErrDetail: "duplicate IP address 10.0.0.1/24 for bridge br-test-1-2", }, { name: "invalid subnet for network bridge", @@ -333,7 +333,7 @@ var testCheckTopoCases = []struct { { Name: "br-test-1", Bridge: "br-test-1-2", - Ipv4: "10.0.1.1", + Ipv4: "10.0.1.1/24", }, }, }, @@ -344,7 +344,7 @@ var testCheckTopoCases = []struct { { Name: "br-test-2", Bridge: "br-test-1-2", - Ipv4: "10.0.0.2", + Ipv4: "10.0.0.2/24", }, }, }, @@ -357,7 +357,7 @@ var testCheckTopoCases = []struct { }, }, expectedErr: true, - expectedErrDetail: "invalid IP address 10.0.1.1 for bridge br-test-1-2: IPv4 address 10.0.1.1 is not in subnet 10.0.0.0/24", + expectedErrDetail: "invalid IP address 10.0.1.1/24 for bridge br-test-1-2: IPv4 address 10.0.1.1/24 is not in subnet 10.0.0.0/24", }, { name: "invalid IP address", @@ -370,7 +370,7 @@ var testCheckTopoCases = []struct { { Name: "br-test-1", Bridge: "br-test-1-2", - Ipv4: "10.0.0.999", + Ipv4: "10.0.0.999/24", }, }, }, @@ -381,7 +381,7 @@ var testCheckTopoCases = []struct { { Name: "br-test-2", Bridge: "br-test-1-2", - Ipv4: "10.0.0.2", + Ipv4: "10.0.0.2/24", }, }, }, @@ -394,7 +394,7 @@ var testCheckTopoCases = []struct { }, }, expectedErr: true, - expectedErrDetail: "invalid IP address 10.0.0.999 for bridge br-test-1-2: invalid IP address: 10.0.0.999", + expectedErrDetail: "invalid IP address 10.0.0.999/24 for bridge br-test-1-2: invalid IP address: 10.0.0.999/24", }, } From 9e55ffe37ce87816586a59c43c8af891e2d3e03d Mon Sep 17 00:00:00 2001 From: Alonza0314 Date: Fri, 17 Apr 2026 04:42:15 +0000 Subject: [PATCH 4/8] feat: topo apply --- .github/workflows/integration-test.yaml | 3 + integration-test/topo/test.sh | 66 ++++++++++++++ integration-test/topo/topo_apply.txt | 1 + internal/topo/apply.go | 109 +++++++++++++++++++++++- 4 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 integration-test/topo/test.sh create mode 100644 integration-test/topo/topo_apply.txt diff --git a/.github/workflows/integration-test.yaml b/.github/workflows/integration-test.yaml index 2afcaef..afde57e 100644 --- a/.github/workflows/integration-test.yaml +++ b/.github/workflows/integration-test.yaml @@ -42,5 +42,8 @@ jobs: - name: Exec Test run: cd integration-test/exec && ./test.sh + - name: Topo Test + run: cd integration-test/topo && ./test.sh + - name: Remove netns Folder run: rm -rf /var/run/netns \ No newline at end of file diff --git a/integration-test/topo/test.sh b/integration-test/topo/test.sh new file mode 100644 index 0000000..fadf76b --- /dev/null +++ b/integration-test/topo/test.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +diff() { + if [ "$2" != "$3" ]; then + echo "[-][FAIL] $1" + echo "Expected:" + echo "$3" + echo "Got:" + echo "$2" + exit 1 + fi + + echo "[+][PASS] $1" +} + +assert_match() { + if ! printf '%s\n' "$2" | grep -Eq "$3"; then + echo "[-][FAIL] $1" + echo "Expected pattern:" + echo "$3" + echo "Got:" + echo "$2" + exit 1 + fi + + echo "[+][PASS] $1" +} + +assert_count() { + count=$(printf '%s\n' "$2" | grep -Ec "$3") + if [ "$count" -ne "$4" ]; then + echo "[-][FAIL] $1" + echo "Expected count: $4" + echo "Pattern:" + echo "$3" + echo "Got count: $count" + echo "Output:" + echo "$2" + exit 1 + fi + + echo "[+][PASS] $1" +} + +topo_apply() { + output=$(../nsctl topo apply topo_template.yaml) + expect=$(cat topo_apply.txt) + + diff "Topo apply" "$output" "$expect" +} + +exec_ping_test() { + output=$(../nsctl exec test-1 -- ping -I br-test-1 -c 3 10.0.0.2) + + assert_match "Exec ping header" "$output" '^PING 10\.0\.0\.2 \(10\.0\.0\.2\) from 10\.0\.0\.1 br-test-1: 56\(84\) bytes of data\.$' + assert_count "Exec ping reply count" "$output" '^64 bytes from 10\.0\.0\.2: icmp_seq=[1-3] ttl=64 time=[0-9.]+ ms$' 3 + assert_match "Exec ping packet loss" "$output" '^3 packets transmitted, 3 received, 0% packet loss, time [0-9]+ms$' + assert_match "Exec ping rtt stats" "$output" '^rtt min/avg/max/mdev = [0-9.]+/[0-9.]+/[0-9.]+/[0-9.]+ ms$' +} + +main() { + topo_apply + exec_ping_test +} + +main "$@" \ No newline at end of file diff --git a/integration-test/topo/topo_apply.txt b/integration-test/topo/topo_apply.txt new file mode 100644 index 0000000..7d203df --- /dev/null +++ b/integration-test/topo/topo_apply.txt @@ -0,0 +1 @@ +Topology applied successfully from file: topo_template.yaml \ No newline at end of file diff --git a/internal/topo/apply.go b/internal/topo/apply.go index f0d7c8b..700a5ce 100644 --- a/internal/topo/apply.go +++ b/internal/topo/apply.go @@ -1,9 +1,116 @@ package topo +import ( + "github.com/Alonza0314/nsctl/internal/namespace" + "github.com/Alonza0314/nsctl/internal/veth" + "github.com/vishvananda/netlink" +) + func Apply(topo *Topology) error { if err := checkTopo(topo); err != nil { return err } + if err := checkExist(topo); err != nil { + return err + } + + if err := addBridges(topo.Networks); err != nil { + return err + } + + if err := addNamespaces(topo.Namespaces); err != nil { + return err + } + + return nil +} + +func addBridges(networks []Network) error { + for _, network := range networks { + br := &netlink.Bridge{ + LinkAttrs: netlink.LinkAttrs{ + Name: network.Name, + }, + } + + if err := netlink.LinkAdd(br); err != nil { + return err + } + + if err := netlink.LinkSetUp(br); err != nil { + return err + } + } + + return nil +} + +func addNamespaces(namespaces []Namespace) error { + for _, ns := range namespaces { + if err := namespace.Create(ns.Name); err != nil { + return err + } + + if err := addVethPair(ns); err != nil { + return err + } + } + + return nil +} + +func addVethPair(ns Namespace) error { + for _, network := range ns.Networks { + vethLink := &netlink.Veth{ + LinkAttrs: netlink.LinkAttrs{ + Name: network.Name, + }, + PeerName: "m-" + network.Name, + } + + if err := netlink.LinkAdd(vethLink); err != nil { + return err + } + + brLink, err := netlink.LinkByName(network.Bridge) + if err != nil { + return err + } + vethNs, err := netlink.LinkByName(network.Name) + if err != nil { + return err + } + peer, err := netlink.LinkByName("m-" + network.Name) + if err != nil { + return err + } + + if err := netlink.LinkSetMaster(peer, brLink); err != nil { + return err + } + if err := netlink.LinkSetUp(peer); err != nil { + return err + } + + nsFd, nsCloseFunc, err := namespace.GetNsFd(ns.Name) + if err != nil { + return err + } + defer nsCloseFunc() + + if err := netlink.LinkSetNsFd(vethNs, int(nsFd)); err != nil { + return err + } + + if err := veth.SetIp(ns.Name, network.Name, network.Ipv4); err != nil { + return err + } + + if err := veth.UpDown(ns.Name, network.Name, true); err != nil { + return err + } + } + return nil -} \ No newline at end of file +} From 30fc8a463b5700012e872b1ce9fe9adc04a3f411 Mon Sep 17 00:00:00 2001 From: Alonza0314 Date: Fri, 17 Apr 2026 04:44:07 +0000 Subject: [PATCH 5/8] fix: chmod +x --- integration-test/topo/test.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 integration-test/topo/test.sh diff --git a/integration-test/topo/test.sh b/integration-test/topo/test.sh old mode 100644 new mode 100755 From 59be761d820731979c83e84e62a4e13631088c63 Mon Sep 17 00:00:00 2001 From: Alonza0314 Date: Fri, 17 Apr 2026 05:17:44 +0000 Subject: [PATCH 6/8] feat: topo delete --- cmd/topo.go | 5 ++ integration-test/topo/test.sh | 8 ++ integration-test/topo/topo_delete.txt | 1 + internal/topo/delete.go | 117 ++++++++++++++++++++++++++ 4 files changed, 131 insertions(+) create mode 100644 integration-test/topo/topo_delete.txt create mode 100644 internal/topo/delete.go diff --git a/cmd/topo.go b/cmd/topo.go index 256351f..8657664 100644 --- a/cmd/topo.go +++ b/cmd/topo.go @@ -41,6 +41,11 @@ func topoFunc(cmd *cobra.Command, args []string) { fmt.Printf("Topology applied successfully from file: %s\n", args[1]) } case "delete": + if err := topo.Delete(&topoStruct); err != nil { + errPrint(err) + } else { + fmt.Printf("Topology deleted successfully from file: %s\n", args[1]) + } default: errFormat(args) } diff --git a/integration-test/topo/test.sh b/integration-test/topo/test.sh index fadf76b..8d4f8fd 100755 --- a/integration-test/topo/test.sh +++ b/integration-test/topo/test.sh @@ -58,9 +58,17 @@ exec_ping_test() { assert_match "Exec ping rtt stats" "$output" '^rtt min/avg/max/mdev = [0-9.]+/[0-9.]+/[0-9.]+/[0-9.]+ ms$' } +topo_delete() { + output=$(../nsctl topo delete topo_template.yaml) + expect=$(cat topo_delete.txt) + + diff "Topo delete" "$output" "$expect" +} + main() { topo_apply exec_ping_test + topo_delete } main "$@" \ No newline at end of file diff --git a/integration-test/topo/topo_delete.txt b/integration-test/topo/topo_delete.txt new file mode 100644 index 0000000..057de59 --- /dev/null +++ b/integration-test/topo/topo_delete.txt @@ -0,0 +1 @@ +Topology deleted successfully from file: topo_template.yaml \ No newline at end of file diff --git a/internal/topo/delete.go b/internal/topo/delete.go new file mode 100644 index 0000000..68248d5 --- /dev/null +++ b/internal/topo/delete.go @@ -0,0 +1,117 @@ +package topo + +import ( + "errors" + "fmt" + "strings" + + "github.com/Alonza0314/nsctl/internal/namespace" + "github.com/Alonza0314/nsctl/internal/veth" + "github.com/vishvananda/netlink" + "github.com/vishvananda/netns" +) + +func Delete(topo *Topology) error { + if err := checkTopo(topo); err != nil { + return err + } + + if err := deleteNamespaces(topo.Namespaces); err != nil { + return fmt.Errorf("failed to delete namespaces: %v", err) + } + + if err := deleteBridges(topo.Networks); err != nil { + return fmt.Errorf("failed to delete bridges: %v", err) + } + + return nil +} + +func deleteNamespaces(nss []Namespace) error { + var errBuilder []error + for _, ns := range nss { + found, err := namespace.GetNs(ns.Name) + if err != nil { + errBuilder = append(errBuilder, fmt.Errorf("failed to get namespace %s: %v", ns.Name, err)) + continue + } + if !found { + errBuilder = append(errBuilder, fmt.Errorf("namespace %s does not exist", ns.Name)) + continue + } + + _, originCloseFunc, err := namespace.GetOriginNs() + if err != nil { + errBuilder = append(errBuilder, fmt.Errorf("failed to get origin namespace file descriptor: %v", err)) + continue + } + nsFd, nsCloseFunc, err := namespace.GetNsFd(ns.Name) + if err != nil { + errBuilder = append(errBuilder, fmt.Errorf("failed to get namespace %s file descriptor: %v", ns.Name, err)) + originCloseFunc() + continue + } + + if err := netns.Set(nsFd); err != nil { + errBuilder = append(errBuilder, fmt.Errorf("failed to set namespace %s: %v", ns.Name, err)) + nsCloseFunc() + originCloseFunc() + continue + } + + for _, network := range ns.Networks { + if err := veth.UpDown(ns.Name, network.Name, false); err != nil { + errBuilder = append(errBuilder, fmt.Errorf("failed to bring down veth for namespace %s and network %s: %v", ns.Name, network.Name, err)) + continue + } + + link, err := netlink.LinkByName(network.Name) + if err != nil { + errBuilder = append(errBuilder, fmt.Errorf("failed to get link %s in namespace %s: %v", network.Name, ns.Name, err)) + continue + } + if err := netlink.LinkDel(link); err != nil { + errBuilder = append(errBuilder, fmt.Errorf("failed to delete link %s in namespace %s: %v", network.Name, ns.Name, err)) + } + } + + nsCloseFunc() + originCloseFunc() + + if err := namespace.Delete(ns.Name); err != nil { + errBuilder = append(errBuilder, fmt.Errorf("failed to delete namespace %s: %v", ns.Name, err)) + } + } + + return errBuild(errBuilder) +} + +func deleteBridges(networks []Network) error { + var errBuilder []error + for _, network := range networks { + link, err := netlink.LinkByName(network.Name) + if err != nil { + errBuilder = append(errBuilder, fmt.Errorf("failed to get bridge %s: %v", network.Name, err)) + continue + } + if err := netlink.LinkDel(link); err != nil { + errBuilder = append(errBuilder, fmt.Errorf("failed to delete bridge %s: %v", network.Name, err)) + } + } + + return errBuild(errBuilder) +} + +func errBuild(errBuilder []error) error { + if len(errBuilder) == 0 { + return nil + } + + var b strings.Builder + b.WriteString("Multiple errors occurred:\n") + for _, err := range errBuilder { + fmt.Fprintf(&b, "- %v\n", err) + } + + return errors.New(b.String()) +} From 5dacb0e6e16860bbebf6b1ad0691bc7343970d67 Mon Sep 17 00:00:00 2001 From: Alonza0314 Date: Fri, 17 Apr 2026 05:33:39 +0000 Subject: [PATCH 7/8] chore: close fd withoud defer --- internal/topo/apply.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/topo/apply.go b/internal/topo/apply.go index 700a5ce..e984005 100644 --- a/internal/topo/apply.go +++ b/internal/topo/apply.go @@ -97,7 +97,6 @@ func addVethPair(ns Namespace) error { if err != nil { return err } - defer nsCloseFunc() if err := netlink.LinkSetNsFd(vethNs, int(nsFd)); err != nil { return err @@ -110,6 +109,8 @@ func addVethPair(ns Namespace) error { if err := veth.UpDown(ns.Name, network.Name, true); err != nil { return err } + + nsCloseFunc() } return nil From 2c7c668cb3f9581a5a443042b5f258d900f3a32e Mon Sep 17 00:00:00 2001 From: Alonza0314 Date: Fri, 17 Apr 2026 06:50:19 +0000 Subject: [PATCH 8/8] feat: pterm --- go.mod | 11 +++++ go.sum | 103 ++++++++++++++++++++++++++++++++++++++++ internal/topo/apply.go | 21 +++++++- internal/topo/delete.go | 65 ++++++++++++------------- internal/topo/topo.go | 11 +++++ 5 files changed, 175 insertions(+), 36 deletions(-) diff --git a/go.mod b/go.mod index 34d4cf7..ab861ac 100644 --- a/go.mod +++ b/go.mod @@ -4,17 +4,23 @@ go 1.25.5 require ( github.com/free-ran-ue/util v0.0.3 + github.com/pterm/pterm v0.12.83 github.com/spf13/cobra v1.10.2 github.com/vishvananda/netlink v1.3.1 github.com/vishvananda/netns v0.0.5 ) require ( + atomicgo.dev/cursor v0.2.0 // indirect + atomicgo.dev/keyboard v0.2.9 // indirect + atomicgo.dev/schedule v0.1.0 // indirect github.com/Alonza0314/logger-go/v2 v2.0.5 // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect + github.com/containerd/console v1.0.5 // indirect github.com/free-ran-ue/free-ran-ue/v2 v2.3.11 // indirect github.com/free5gc/aper v1.1.0 // indirect github.com/free5gc/nas v1.2.2 // indirect @@ -29,11 +35,14 @@ require ( github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/gookit/color v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect @@ -44,11 +53,13 @@ require ( github.com/tim-ywliu/nested-logrus-formatter v1.3.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect golang.org/x/arch v0.22.0 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/sys v0.41.0 // indirect + golang.org/x/term v0.40.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 59036a4..4caf8f6 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,36 @@ +atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg= +atomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ= +atomicgo.dev/cursor v0.2.0 h1:H6XN5alUJ52FZZUkI7AlJbUc1aW38GWZalpYRPpoPOw= +atomicgo.dev/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= +atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= +atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= +atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= +atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= github.com/Alonza0314/logger-go/v2 v2.0.5 h1:ZCOHbPEZ4tXrS7PC3ujOKOwILKeRC3sOW1/1ItLECoo= github.com/Alonza0314/logger-go/v2 v2.0.5/go.mod h1:fRFP+N39GOCRbuYMDc5PI9KJvzTyxpGNhpyHkIhIsoY= +github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= +github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= +github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= +github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= +github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= +github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= +github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= +github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= +github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= +github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= +github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -49,20 +72,37 @@ github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArs github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0= +github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E= +github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= +github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= +github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA= +github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= +github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= +github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -72,13 +112,25 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= +github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= +github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= +github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= +github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= +github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= +github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= +github.com/pterm/pterm v0.12.83 h1:ie+YmGmA727VuhxBlyGr74Ks+7McV6kT99IB8EU80aA= +github.com/pterm/pterm v0.12.83/go.mod h1:xlgc6bFWyJIMtmLJvGim+L7jhSReilOlOnodeIYe4Tk= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= @@ -90,6 +142,8 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -107,6 +161,10 @@ github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= @@ -114,25 +172,70 @@ go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/topo/apply.go b/internal/topo/apply.go index e984005..8906cd4 100644 --- a/internal/topo/apply.go +++ b/internal/topo/apply.go @@ -3,6 +3,7 @@ package topo import ( "github.com/Alonza0314/nsctl/internal/namespace" "github.com/Alonza0314/nsctl/internal/veth" + "github.com/pterm/pterm" "github.com/vishvananda/netlink" ) @@ -28,6 +29,11 @@ func Apply(topo *Topology) error { func addBridges(networks []Network) error { for _, network := range networks { + spinner, err := pterm.DefaultSpinner.Start("Creating bridge " + network.Name + "...") + if err != nil { + return err + } + br := &netlink.Bridge{ LinkAttrs: netlink.LinkAttrs{ Name: network.Name, @@ -35,12 +41,16 @@ func addBridges(networks []Network) error { } if err := netlink.LinkAdd(br); err != nil { + spinner.Fail("Failed to create bridge " + network.Name + ": " + err.Error()) return err } if err := netlink.LinkSetUp(br); err != nil { + spinner.Fail("Failed to set bridge " + network.Name + " up: " + err.Error()) return err } + + spinner.Success("Bridge " + network.Name + " created") } return nil @@ -48,13 +58,22 @@ func addBridges(networks []Network) error { func addNamespaces(namespaces []Namespace) error { for _, ns := range namespaces { + spinner, err := pterm.DefaultSpinner.Start("Creating namespace " + ns.Name + "...") + if err != nil { + return err + } + if err := namespace.Create(ns.Name); err != nil { + spinner.Fail("Failed to create namespace " + ns.Name + ": " + err.Error()) return err } if err := addVethPair(ns); err != nil { + spinner.Fail("Failed to add veth pair for namespace " + ns.Name + ": " + err.Error()) return err } + + spinner.Success("Namespace " + ns.Name + " created") } return nil @@ -109,7 +128,7 @@ func addVethPair(ns Namespace) error { if err := veth.UpDown(ns.Name, network.Name, true); err != nil { return err } - + nsCloseFunc() } diff --git a/internal/topo/delete.go b/internal/topo/delete.go index 68248d5..8392bb1 100644 --- a/internal/topo/delete.go +++ b/internal/topo/delete.go @@ -1,12 +1,9 @@ package topo import ( - "errors" - "fmt" - "strings" - "github.com/Alonza0314/nsctl/internal/namespace" "github.com/Alonza0314/nsctl/internal/veth" + "github.com/pterm/pterm" "github.com/vishvananda/netlink" "github.com/vishvananda/netns" ) @@ -17,43 +14,48 @@ func Delete(topo *Topology) error { } if err := deleteNamespaces(topo.Namespaces); err != nil { - return fmt.Errorf("failed to delete namespaces: %v", err) + return err } if err := deleteBridges(topo.Networks); err != nil { - return fmt.Errorf("failed to delete bridges: %v", err) + return err } return nil } func deleteNamespaces(nss []Namespace) error { - var errBuilder []error for _, ns := range nss { + spinner, err := pterm.DefaultSpinner.Start("Deleting namespace " + ns.Name + "...") + if err != nil { + spinner.Fail("Failed to start spinner for namespace " + ns.Name + ": " + err.Error()) + continue + } + found, err := namespace.GetNs(ns.Name) if err != nil { - errBuilder = append(errBuilder, fmt.Errorf("failed to get namespace %s: %v", ns.Name, err)) + spinner.Fail("Failed to get namespace " + ns.Name + ": " + err.Error()) continue } if !found { - errBuilder = append(errBuilder, fmt.Errorf("namespace %s does not exist", ns.Name)) + spinner.Fail("Namespace " + ns.Name + " does not exist") continue } _, originCloseFunc, err := namespace.GetOriginNs() if err != nil { - errBuilder = append(errBuilder, fmt.Errorf("failed to get origin namespace file descriptor: %v", err)) + spinner.Fail("Failed to get origin namespace file descriptor: " + err.Error()) continue } nsFd, nsCloseFunc, err := namespace.GetNsFd(ns.Name) if err != nil { - errBuilder = append(errBuilder, fmt.Errorf("failed to get namespace %s file descriptor: %v", ns.Name, err)) + spinner.Fail("Failed to get namespace " + ns.Name + " file descriptor: " + err.Error()) originCloseFunc() continue } if err := netns.Set(nsFd); err != nil { - errBuilder = append(errBuilder, fmt.Errorf("failed to set namespace %s: %v", ns.Name, err)) + spinner.Fail("Failed to set namespace " + ns.Name + ": " + err.Error()) nsCloseFunc() originCloseFunc() continue @@ -61,17 +63,17 @@ func deleteNamespaces(nss []Namespace) error { for _, network := range ns.Networks { if err := veth.UpDown(ns.Name, network.Name, false); err != nil { - errBuilder = append(errBuilder, fmt.Errorf("failed to bring down veth for namespace %s and network %s: %v", ns.Name, network.Name, err)) + spinner.Fail("Failed to bring down veth for namespace " + ns.Name + " and network " + network.Name + ": " + err.Error()) continue } link, err := netlink.LinkByName(network.Name) if err != nil { - errBuilder = append(errBuilder, fmt.Errorf("failed to get link %s in namespace %s: %v", network.Name, ns.Name, err)) + spinner.Fail("Failed to get link " + network.Name + " in namespace " + ns.Name + ": " + err.Error()) continue } if err := netlink.LinkDel(link); err != nil { - errBuilder = append(errBuilder, fmt.Errorf("failed to delete link %s in namespace %s: %v", network.Name, ns.Name, err)) + spinner.Fail("Failed to delete link " + network.Name + " in namespace " + ns.Name + ": " + err.Error()) } } @@ -79,39 +81,32 @@ func deleteNamespaces(nss []Namespace) error { originCloseFunc() if err := namespace.Delete(ns.Name); err != nil { - errBuilder = append(errBuilder, fmt.Errorf("failed to delete namespace %s: %v", ns.Name, err)) + spinner.Fail("Failed to delete namespace " + ns.Name + ": " + err.Error()) } + spinner.Success("Namespace " + ns.Name + " deleted") } - return errBuild(errBuilder) + return nil } func deleteBridges(networks []Network) error { - var errBuilder []error for _, network := range networks { + spinner, err := pterm.DefaultSpinner.Start("Deleting bridge " + network.Name + "...") + if err != nil { + spinner.Fail("Failed to start spinner for bridge " + network.Name + ": " + err.Error()) + continue + } + link, err := netlink.LinkByName(network.Name) if err != nil { - errBuilder = append(errBuilder, fmt.Errorf("failed to get bridge %s: %v", network.Name, err)) + spinner.Fail("Failed to get bridge " + network.Name + ": " + err.Error()) continue } if err := netlink.LinkDel(link); err != nil { - errBuilder = append(errBuilder, fmt.Errorf("failed to delete bridge %s: %v", network.Name, err)) + spinner.Fail("Failed to delete bridge " + network.Name + ": " + err.Error()) } + spinner.Success("Bridge " + network.Name + " deleted") } - return errBuild(errBuilder) -} - -func errBuild(errBuilder []error) error { - if len(errBuilder) == 0 { - return nil - } - - var b strings.Builder - b.WriteString("Multiple errors occurred:\n") - for _, err := range errBuilder { - fmt.Fprintf(&b, "- %v\n", err) - } - - return errors.New(b.String()) + return nil } diff --git a/internal/topo/topo.go b/internal/topo/topo.go index 1884863..a0c4653 100644 --- a/internal/topo/topo.go +++ b/internal/topo/topo.go @@ -5,16 +5,27 @@ import ( "net" "github.com/Alonza0314/nsctl/internal/namespace" + "github.com/pterm/pterm" ) func checkTopo(topo *Topology) error { + spinner, err := pterm.DefaultSpinner.Start("Checking topology namespaces...") + if err != nil { + return err + } if err := checkTopoNamespace(topo); err != nil { return err } + spinner.Success("Topology namespaces check passed") + spinner, err = pterm.DefaultSpinner.Start("Checking topology networks...") + if err != nil { + return err + } if err := checkTopoNetwork(topo); err != nil { return err } + spinner.Success("Topology networks check passed") return nil }