From 31c36300b85a7900d5102b425995d36f6421531c Mon Sep 17 00:00:00 2001 From: Shiming Zhang Date: Fri, 24 May 2024 12:07:33 +0800 Subject: [PATCH] Support command like kubectl Signed-off-by: Shiming Zhang --- cmd/augerctl/main.go | 31 ++++ go.mod | 22 ++- go.sum | 55 ++++-- pkg/client/client.go | 213 +++++++++++++++++++++++ pkg/client/client_delete.go | 63 +++++++ pkg/client/client_get.go | 134 ++++++++++++++ pkg/client/client_put.go | 65 +++++++ pkg/client/client_test.go | 198 +++++++++++++++++++++ pkg/client/client_watch.go | 69 ++++++++ pkg/cmd/ctl/ctl.go | 83 +++++++++ pkg/cmd/ctl/del.go | 115 ++++++++++++ pkg/cmd/ctl/get.go | 199 +++++++++++++++++++++ pkg/cmd/ctl/global.go | 337 ++++++++++++++++++++++++++++++++++++ pkg/cmd/ctl/put.go | 239 +++++++++++++++++++++++++ 14 files changed, 1806 insertions(+), 17 deletions(-) create mode 100644 cmd/augerctl/main.go create mode 100644 pkg/client/client.go create mode 100644 pkg/client/client_delete.go create mode 100644 pkg/client/client_get.go create mode 100644 pkg/client/client_put.go create mode 100644 pkg/client/client_test.go create mode 100644 pkg/client/client_watch.go create mode 100644 pkg/cmd/ctl/ctl.go create mode 100644 pkg/cmd/ctl/del.go create mode 100644 pkg/cmd/ctl/get.go create mode 100644 pkg/cmd/ctl/global.go create mode 100644 pkg/cmd/ctl/put.go diff --git a/cmd/augerctl/main.go b/cmd/augerctl/main.go new file mode 100644 index 0000000..f27bf34 --- /dev/null +++ b/cmd/augerctl/main.go @@ -0,0 +1,31 @@ +/* +Copyright 2024 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 main + +import ( + "fmt" + "os" + + "github.com/etcd-io/auger/pkg/cmd/ctl" +) + +func main() { + if err := ctl.NewCtlCommand().Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod index 7953c47..691f10c 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,13 @@ module github.com/etcd-io/auger go 1.22.4 require ( + github.com/bgentry/speakeasy v0.1.0 github.com/google/safetext v0.0.0-20220914124124-e18e3fe012bf github.com/spf13/cobra v1.8.1 go.etcd.io/bbolt v1.3.10 - go.etcd.io/etcd/api/v3 v3.5.14 + go.etcd.io/etcd/api/v3 v3.5.15 + go.etcd.io/etcd/client/pkg/v3 v3.5.15 + go.etcd.io/etcd/client/v3 v3.5.15 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.30.2 k8s.io/apimachinery v0.30.2 @@ -14,6 +17,8 @@ require ( ) require ( + github.com/coreos/go-semver v0.3.0 // indirect + github.com/coreos/go-systemd/v22 v22.3.2 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -24,10 +29,17 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.9.0 // indirect - golang.org/x/net v0.27.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.17.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect + google.golang.org/grpc v1.59.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect diff --git a/go.sum b/go.sum index 5ed083c..abfb97f 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,16 @@ +github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -31,6 +38,9 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w 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/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= @@ -42,14 +52,25 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= -go.etcd.io/etcd/api/v3 v3.5.14 h1:vHObSCxyB9zlF60w7qzAdTcGaglbJOpSj1Xj9+WGxq0= -go.etcd.io/etcd/api/v3 v3.5.14/go.mod h1:BmtWcRlQvwa1h3G2jvKYwIQy4PkHlDej5t7uLMUdJUU= +go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk= +go.etcd.io/etcd/api/v3 v3.5.15/go.mod h1:N9EhGzXq58WuMllgH9ZvnEr7SI9pS0k0+DHZezGp7jM= +go.etcd.io/etcd/client/pkg/v3 v3.5.15 h1:fo0HpWz/KlHGMCC+YejpiCmyWDEuIpnTDzpJLB5fWlA= +go.etcd.io/etcd/client/pkg/v3 v3.5.15/go.mod h1:mXDI4NAOwEiszrHCb0aqfAYNCrZP4e9hRca3d1YK8EU= +go.etcd.io/etcd/client/v3 v3.5.15 h1:23M0eY4Fd/inNv1ZfU3AxrbbOdW79r9V9Rl62Nm6ip4= +go.etcd.io/etcd/client/v3 v3.5.15/go.mod h1:CLSJxrYjvLtHsrPKsy7LmZEE+DK2ktfd2bN4RhBMwlU= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -59,22 +80,22 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 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.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -83,8 +104,16 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= +google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q= +google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 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= @@ -93,6 +122,8 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v2 v2.2.8/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= k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI= diff --git a/pkg/client/client.go b/pkg/client/client.go new file mode 100644 index 0000000..9c84521 --- /dev/null +++ b/pkg/client/client.go @@ -0,0 +1,213 @@ +/* +Copyright 2024 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 client + +import ( + "context" + "strings" + + "github.com/etcd-io/auger/pkg/encoding" + + "k8s.io/apimachinery/pkg/runtime/schema" + + clientv3 "go.etcd.io/etcd/client/v3" +) + +// Client is an interface that defines the operations that can be performed on an etcd client. +type Client interface { + // Get is a method that retrieves a key-value pair from the etcd server. + // It returns the revision of the key-value pair + Get(ctx context.Context, prefix string, opOpts ...OpOption) (rev int64, err error) + + // Watch is a method that watches for changes to a key-value pair on the etcd server. + Watch(ctx context.Context, prefix string, opOpts ...OpOption) error + + // Delete is a method that deletes a key-value pair from the etcd server. + Delete(ctx context.Context, prefix string, opOpts ...OpOption) error + + // Put is a method that sets a key-value pair on the etcd server. + Put(ctx context.Context, prefix string, value []byte, opOpts ...OpOption) error +} + +// client is the etcd client. +type client struct { + client *clientv3.Client +} + +type Config = clientv3.Config + +// NewClient creates a new etcd client. +func NewClient(conf Config) (Client, error) { + cli, err := clientv3.New(conf) + if err != nil { + return nil, err + } + return &client{ + client: cli, + }, nil +} + +func (c *client) getPrefix(prefix string, opt Op) (string, bool, error) { + var single bool + var arr [4]string + s := arr[:0] + s = append(s, prefix) + + if !opt.gr.Empty() { + p, err := PrefixFromGR(opt.gr) + if err != nil { + return "", false, err + } + s = append(s, p) + if opt.namespace != "" { + s = append(s, opt.namespace) + } + if opt.name != "" { + s = append(s, opt.name) + single = true + } + } + return strings.Join(s, "/"), single, nil +} + +// Op is the option for the operation. +type Op struct { + gr schema.GroupResource + name string + namespace string + response func(kv *KeyValue) error + pageLimit int64 + keysOnly bool + revision int64 +} + +// OpOption is the option for the operation. +type OpOption func(*Op) + +// WithGR sets the gr for the target. +func WithGR(gr schema.GroupResource) OpOption { + return func(o *Op) { + o.gr = gr + } +} + +// WithName sets the name and namespace for the target. +func WithName(name, namespace string) OpOption { + return func(o *Op) { + o.name = name + o.namespace = namespace + } +} + +// WithResponse sets the response callback for the target. +func WithResponse(response func(kv *KeyValue) error) OpOption { + return func(o *Op) { + o.response = response + } +} + +// WithPageLimit sets the page limit for the target. +func WithPageLimit(pageLimit int64) OpOption { + return func(o *Op) { + o.pageLimit = pageLimit + } +} + +// WithKeysOnly sets the keys only for the target. +func WithKeysOnly() OpOption { + return func(o *Op) { + o.keysOnly = true + } +} + +// WithRevision sets the revision for the target. +func WithRevision(revision int64) OpOption { + return func(o *Op) { + o.revision = revision + } +} + +func opOption(opts []OpOption) Op { + var opt Op + for _, o := range opts { + o(&opt) + } + return opt +} + +// KeyValue is the key-value pair. +type KeyValue struct { + Key []byte + Value []byte + PrevValue []byte +} + +// specialDefaultResourcePrefixes are prefixes compiled into Kubernetes. +// see k8s.io/kubernetes/pkg/kubeapiserver/default_storage_factory_builder.go +var specialDefaultResourcePrefixes = map[schema.GroupResource]string{ + {Group: "", Resource: "replicationcontrollers"}: "controllers", + {Group: "", Resource: "endpoints"}: "services/endpoints", + {Group: "", Resource: "services"}: "services/specs", + {Group: "", Resource: "nodes"}: "minions", + {Group: "extensions", Resource: "ingresses"}: "ingress", + {Group: "networking.k8s.io", Resource: "ingresses"}: "ingress", +} + +var specialDefaultMediaTypes = map[string]struct{}{ + "apiextensions.k8s.io": {}, + "apiregistration.k8s.io": {}, +} + +// PrefixFromGR returns the prefix of the given GroupResource. +func PrefixFromGR(gr schema.GroupResource) (prefix string, err error) { + groupPrefix := false + + if _, ok := specialDefaultMediaTypes[gr.Group]; ok { + groupPrefix = true + } else if !strings.Contains(gr.Group, ".") || strings.HasSuffix(gr.Group, ".k8s.io") { + // custom resources + groupPrefix = false + } else { + // builtin resource + groupPrefix = true + } + + if prefix, ok := specialDefaultResourcePrefixes[gr]; ok { + return prefix, nil + } + + if groupPrefix { + return gr.Group + "/" + gr.Resource, nil + } + + return gr.Resource, nil +} + +// MediaTypeFromGR returns the media type of the given GroupResource. +func MediaTypeFromGR(gr schema.GroupResource) (mediaType string, err error) { + mediaType = encoding.JsonMediaType + + if _, ok := specialDefaultMediaTypes[gr.Group]; ok { + return mediaType, nil + } + + if !strings.Contains(gr.Group, ".") || strings.HasSuffix(gr.Group, ".k8s.io") { + return encoding.StorageBinaryMediaType, nil + } + + return mediaType, nil +} diff --git a/pkg/client/client_delete.go b/pkg/client/client_delete.go new file mode 100644 index 0000000..d04ed71 --- /dev/null +++ b/pkg/client/client_delete.go @@ -0,0 +1,63 @@ +/* +Copyright 2024 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 client + +import ( + "context" + + clientv3 "go.etcd.io/etcd/client/v3" +) + +func (c *client) Delete(ctx context.Context, prefix string, opOpts ...OpOption) error { + opt := opOption(opOpts) + prefix, _, err := c.getPrefix(prefix, opt) + if err != nil { + return err + } + + opts := []clientv3.OpOption{} + + if opt.name == "" { + opts = append(opts, clientv3.WithPrefix()) + } + + if opt.response != nil { + if opt.keysOnly { + opts = append(opts, clientv3.WithKeysOnly()) + } + opts = append(opts, clientv3.WithPrevKV()) + } + + resp, err := c.client.Delete(ctx, prefix, opts...) + if err != nil { + return err + } + + if opt.response != nil { + for _, kv := range resp.PrevKvs { + r := &KeyValue{ + Key: kv.Key, + PrevValue: kv.Value, + } + err = opt.response(r) + if err != nil { + return err + } + } + } + return nil +} diff --git a/pkg/client/client_get.go b/pkg/client/client_get.go new file mode 100644 index 0000000..247cc9a --- /dev/null +++ b/pkg/client/client_get.go @@ -0,0 +1,134 @@ +/* +Copyright 2024 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 client + +import ( + "context" + "fmt" + + clientv3 "go.etcd.io/etcd/client/v3" +) + +func (c *client) Get(ctx context.Context, prefix string, opOpts ...OpOption) (rev int64, err error) { + opt := opOption(opOpts) + if opt.response == nil { + return 0, fmt.Errorf("response is required") + } + + prefix, single, err := c.getPrefix(prefix, opt) + if err != nil { + return 0, err + } + + opts := []clientv3.OpOption{} + if opt.keysOnly { + opts = append(opts, clientv3.WithKeysOnly()) + } + + if single || opt.pageLimit == 0 { + if !single { + opts = append(opts, clientv3.WithPrefix()) + } + resp, err := c.client.Get(ctx, prefix, opts...) + if err != nil { + return 0, err + } + for _, kv := range resp.Kvs { + r := &KeyValue{ + Key: kv.Key, + Value: kv.Value, + } + err := opt.response(r) + if err != nil { + return 0, err + } + } + return resp.Header.Revision, nil + } + + respchan := make(chan clientv3.GetResponse, 10) + errchan := make(chan error, 1) + var revision int64 + + go func() { + defer close(respchan) + defer close(errchan) + + var key string + + opts := append(opts, clientv3.WithLimit(opt.pageLimit)) + if opt.revision != 0 { + revision = opt.revision + opts = append(opts, clientv3.WithRev(revision)) + } + + if len(prefix) == 0 { + // If len(s.prefix) == 0, we will sync the entire key-value space. + // We then range from the smallest key (0x00) to the end. + opts = append(opts, clientv3.WithFromKey()) + key = "\x00" + } else { + // If len(s.prefix) != 0, we will sync key-value space with given prefix. + // We then range from the prefix to the next prefix if exists. Or we will + // range from the prefix to the end if the next prefix does not exists. + opts = append(opts, clientv3.WithRange(clientv3.GetPrefixRangeEnd(prefix))) + key = prefix + } + + for { + resp, err := c.client.Get(ctx, key, opts...) + if err != nil { + errchan <- err + return + } + + respchan <- *resp + + if revision == 0 { + revision = resp.Header.Revision + opts = append(opts, clientv3.WithRev(resp.Header.Revision)) + } + + if !resp.More { + return + } + + // move to next key + key = string(append(resp.Kvs[len(resp.Kvs)-1].Key, 0)) + } + }() + + for resp := range respchan { + for _, kv := range resp.Kvs { + r := &KeyValue{ + Key: kv.Key, + Value: kv.Value, + } + err := opt.response(r) + if err != nil { + return 0, err + } + } + } + + err = <-errchan + if err != nil { + return 0, err + } + + return revision, nil +} diff --git a/pkg/client/client_put.go b/pkg/client/client_put.go new file mode 100644 index 0000000..8be9edb --- /dev/null +++ b/pkg/client/client_put.go @@ -0,0 +1,65 @@ +/* +Copyright 2024 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 client + +import ( + "context" + "fmt" + + clientv3 "go.etcd.io/etcd/client/v3" +) + +func (c *client) Put(ctx context.Context, prefix string, value []byte, opOpts ...OpOption) error { + opt := opOption(opOpts) + prefix, single, err := c.getPrefix(prefix, opt) + if err != nil { + return err + } + if !single { + return fmt.Errorf("put only support single") + } + + opts := []clientv3.OpOption{} + + if opt.response != nil { + if opt.keysOnly { + opts = append(opts, clientv3.WithKeysOnly()) + } + opts = append(opts, clientv3.WithPrevKV()) + } + + resp, err := c.client.Put(ctx, prefix, string(value), opts...) + if err != nil { + return err + } + + if opt.response != nil { + var r *KeyValue + if resp.PrevKv != nil { + r = &KeyValue{ + Key: resp.PrevKv.Key, + Value: value, + PrevValue: resp.PrevKv.Value, + } + } + err = opt.response(r) + if err != nil { + return err + } + } + return nil +} diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go new file mode 100644 index 0000000..604463b --- /dev/null +++ b/pkg/client/client_test.go @@ -0,0 +1,198 @@ +/* +Copyright 2024 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 client + +import ( + "testing" + + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/etcd-io/auger/pkg/encoding" +) + +func TestPrefixFromGR(t *testing.T) { + type args struct { + gr schema.GroupResource + } + tests := []struct { + name string + args args + wantPrefix string + wantErr bool + }{ + { + name: "pod", + args: args{ + gr: schema.GroupResource{ + Group: "", + Resource: "pods", + }, + }, + wantPrefix: "pods", + wantErr: false, + }, + { + name: "deployment", + args: args{ + gr: schema.GroupResource{ + Group: "apps", + Resource: "deployments", + }, + }, + wantPrefix: "deployments", + wantErr: false, + }, + { + name: "service", + args: args{ + gr: schema.GroupResource{ + Group: "", + Resource: "services", + }, + }, + wantPrefix: "services/specs", + wantErr: false, + }, + { + name: "ingress", + args: args{ + gr: schema.GroupResource{ + Group: "networking.k8s.io", + Resource: "ingresses", + }, + }, + wantPrefix: "ingress", + }, + { + name: "apiextensions.k8s.io", + args: args{ + gr: schema.GroupResource{ + Group: "apiextensions.k8s.io", + Resource: "customresourcedefinitions", + }, + }, + wantPrefix: "apiextensions.k8s.io/customresourcedefinitions", + }, + { + name: "x-k8s.io", + args: args{ + gr: schema.GroupResource{ + Group: "auger.x-k8s.io", + Resource: "foo", + }, + }, + wantPrefix: "auger.x-k8s.io/foo", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotPrefix, err := PrefixFromGR(tt.args.gr) + if (err != nil) != tt.wantErr { + t.Errorf("PrefixFromGR() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotPrefix != tt.wantPrefix { + t.Errorf("PrefixFromGR() gotPrefix = %v, want %v", gotPrefix, tt.wantPrefix) + } + }) + } +} + +func TestMediaTypeFromGR(t *testing.T) { + type args struct { + gr schema.GroupResource + } + tests := []struct { + name string + args args + wantMediaType string + wantErr bool + }{ + { + name: "pod", + args: args{ + gr: schema.GroupResource{ + Group: "", + Resource: "pods", + }, + }, + wantMediaType: encoding.StorageBinaryMediaType, + }, + { + name: "deployment", + args: args{ + gr: schema.GroupResource{ + Group: "apps", + Resource: "deployments", + }, + }, + wantMediaType: encoding.StorageBinaryMediaType, + }, + { + name: "service", + args: args{ + gr: schema.GroupResource{ + Group: "", + Resource: "services", + }, + }, + wantMediaType: encoding.StorageBinaryMediaType, + }, + { + name: "ingress", + args: args{ + gr: schema.GroupResource{ + Group: "networking.k8s.io", + Resource: "ingresses", + }, + }, + wantMediaType: encoding.StorageBinaryMediaType, + }, + { + name: "apiextensions.k8s.io", + args: args{ + gr: schema.GroupResource{ + Group: "apiextensions.k8s.io", + Resource: "customresourcedefinitions", + }, + }, + wantMediaType: encoding.JsonMediaType, + }, + { + name: "x-k8s.io", + args: args{ + gr: schema.GroupResource{ + Group: "auger.x-k8s.io", + Resource: "foo", + }, + }, + wantMediaType: encoding.JsonMediaType, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotMediaType, err := MediaTypeFromGR(tt.args.gr) + if (err != nil) != tt.wantErr { + t.Errorf("MediaTypeFromGR() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotMediaType != tt.wantMediaType { + t.Errorf("MediaTypeFromGR() gotMediaType = %v, want %v", gotMediaType, tt.wantMediaType) + } + }) + } +} diff --git a/pkg/client/client_watch.go b/pkg/client/client_watch.go new file mode 100644 index 0000000..75b66fb --- /dev/null +++ b/pkg/client/client_watch.go @@ -0,0 +1,69 @@ +/* +Copyright 2024 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 client + +import ( + "context" + "fmt" + + clientv3 "go.etcd.io/etcd/client/v3" +) + +func (c *client) Watch(ctx context.Context, prefix string, opOpts ...OpOption) error { + opt := opOption(opOpts) + if opt.response == nil { + return fmt.Errorf("response is required") + } + + prefix, single, err := c.getPrefix(prefix, opt) + if err != nil { + return err + } + + opts := []clientv3.OpOption{} + if opt.keysOnly { + opts = append(opts, clientv3.WithKeysOnly()) + } + + if !single { + opts = append(opts, clientv3.WithPrefix()) + } + + if opt.revision != 0 { + opts = append(opts, clientv3.WithRev(opt.revision)) + } + + opts = append(opts, clientv3.WithPrevKV()) + + watchChan := c.client.Watch(ctx, prefix, opts...) + for watchResp := range watchChan { + for _, event := range watchResp.Events { + r := &KeyValue{ + Key: event.Kv.Key, + Value: event.Kv.Value, + } + if event.PrevKv != nil { + r.PrevValue = event.PrevKv.Value + } + err := opt.response(r) + if err != nil { + return err + } + } + } + return nil +} diff --git a/pkg/cmd/ctl/ctl.go b/pkg/cmd/ctl/ctl.go new file mode 100644 index 0000000..0cb44e3 --- /dev/null +++ b/pkg/cmd/ctl/ctl.go @@ -0,0 +1,83 @@ +/* +Copyright 2024 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 ctl is A simple command line client for directly access data objects stored in etcd by Kubernetes. +package ctl + +import ( + "time" + + "github.com/spf13/cobra" + + "go.etcd.io/etcd/client/pkg/v3/transport" +) + +type flagpole struct { + Insecure bool + InsecureSkipVerify bool + InsecureDiscovery bool + Endpoints []string + DialTimeout time.Duration + CommandTimeOut time.Duration + KeepAliveTime time.Duration + KeepAliveTimeout time.Duration + DNSClusterServiceName string + + TLS transport.TLSInfo + + User string + Password string +} + +// NewCtlCommand returns a new cobra.Command for use ctl +func NewCtlCommand() *cobra.Command { + flags := &flagpole{} + const ( + defaultDialTimeout = 2 * time.Second + defaultCommandTimeOut = 5 * time.Second + defaultKeepAliveTime = 2 * time.Second + defaultKeepAliveTimeOut = 6 * time.Second + ) + + cmd := &cobra.Command{ + Use: "augerctl", + Short: "A simple command line client for directly access data objects stored in etcd by Kubernetes.", + } + cmd.PersistentFlags().StringSliceVar(&flags.Endpoints, "endpoints", []string{"127.0.0.1:2379"}, "gRPC endpoints") + + cmd.PersistentFlags().DurationVar(&flags.DialTimeout, "dial-timeout", defaultDialTimeout, "dial timeout for client connections") + cmd.PersistentFlags().DurationVar(&flags.CommandTimeOut, "command-timeout", defaultCommandTimeOut, "timeout for short running command (excluding dial timeout)") + cmd.PersistentFlags().DurationVar(&flags.KeepAliveTime, "keepalive-time", defaultKeepAliveTime, "keepalive time for client connections") + cmd.PersistentFlags().DurationVar(&flags.KeepAliveTimeout, "keepalive-timeout", defaultKeepAliveTimeOut, "keepalive timeout for client connections") + + cmd.PersistentFlags().BoolVar(&flags.Insecure, "insecure-transport", true, "disable transport security for client connections") + cmd.PersistentFlags().BoolVar(&flags.InsecureDiscovery, "insecure-discovery", true, "accept insecure SRV records describing cluster endpoints") + cmd.PersistentFlags().BoolVar(&flags.InsecureSkipVerify, "insecure-skip-tls-verify", false, "skip server certificate verification (CAUTION: this option should be enabled only for testing purposes)") + cmd.PersistentFlags().StringVar(&flags.TLS.CertFile, "cert", "", "identify secure client using this TLS certificate file") + cmd.PersistentFlags().StringVar(&flags.TLS.KeyFile, "key", "", "identify secure client using this TLS key file") + cmd.PersistentFlags().StringVar(&flags.TLS.TrustedCAFile, "cacert", "", "verify certificates of TLS-enabled secure servers using this CA bundle") + cmd.PersistentFlags().StringVar(&flags.User, "user", "", "username[:password] for authentication (prompt if password is not supplied)") + cmd.PersistentFlags().StringVar(&flags.Password, "password", "", "password for authentication (if this option is used, --user option shouldn't include password)") + cmd.PersistentFlags().StringVarP(&flags.TLS.ServerName, "discovery-srv", "d", "", "domain name to query for SRV records describing cluster endpoints") + cmd.PersistentFlags().StringVarP(&flags.DNSClusterServiceName, "discovery-srv-name", "", "", "service name to query when using DNS discovery") + + cmd.AddCommand( + newCtlGetCommand(), + newCtlDelCommand(), + newCtlPutCommand(), + ) + return cmd +} diff --git a/pkg/cmd/ctl/del.go b/pkg/cmd/ctl/del.go new file mode 100644 index 0000000..2ba639e --- /dev/null +++ b/pkg/cmd/ctl/del.go @@ -0,0 +1,115 @@ +/* +Copyright 2024 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 ctl + +import ( + "context" + "fmt" + "os" + + "github.com/etcd-io/auger/pkg/client" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type delFlagpole struct { + Namespace string + Output string + Prefix string +} + +func newCtlDelCommand() *cobra.Command { + flags := &delFlagpole{} + + cmd := &cobra.Command{ + Args: cobra.RangeArgs(0, 2), + Use: "del [resource] [name]", + Short: "Deletes the resource of k8s in etcd", + RunE: func(cmd *cobra.Command, args []string) error { + etcdclient, err := clientFromCmd(cmd) + if err != nil { + return err + } + err = delCommand(cmd.Context(), etcdclient, flags, args) + + if err != nil { + return fmt.Errorf("%v: %w", args, err) + } + return nil + }, + } + + cmd.Flags().StringVarP(&flags.Output, "output", "o", "key", "output format. One of: (key, none).") + cmd.Flags().StringVarP(&flags.Namespace, "namespace", "n", "", "namespace of resource") + cmd.Flags().StringVar(&flags.Prefix, "prefix", "/registry", "prefix to prepend to the resource") + return cmd +} + +func delCommand(ctx context.Context, etcdclient client.Client, flags *delFlagpole, args []string) error { + var targetGr schema.GroupResource + var targetName string + var targetNamespace string + if len(args) != 0 { + // TODO: Support get information from CRD and scheme.Codecs + // Support short name + // Check for namespaced + + gr := schema.ParseGroupResource(args[0]) + if gr.Empty() { + return fmt.Errorf("invalid resource %q", args[0]) + } + targetGr = gr + targetNamespace = flags.Namespace + if len(args) >= 2 { + targetName = args[1] + } + } + + var count int + var response func(kv *client.KeyValue) error + if flags.Output == "key" { + response = func(kv *client.KeyValue) error { + count++ + fmt.Fprintf(os.Stdout, "%s\n", kv.Key) + return nil + } + } + + opOpts := []client.OpOption{ + client.WithName(targetName, targetNamespace), + client.WithGR(targetGr), + } + + if response != nil { + opOpts = append(opOpts, + client.WithKeysOnly(), + client.WithResponse(response), + ) + } + + err := etcdclient.Delete(ctx, flags.Prefix, + opOpts..., + ) + if err != nil { + return err + } + + if flags.Output == "key" { + fmt.Fprintf(os.Stderr, "delete %d keys\n", count) + } + return nil +} diff --git a/pkg/cmd/ctl/get.go b/pkg/cmd/ctl/get.go new file mode 100644 index 0000000..4a446f3 --- /dev/null +++ b/pkg/cmd/ctl/get.go @@ -0,0 +1,199 @@ +/* +Copyright 2024 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 ctl + +import ( + "context" + "fmt" + "os" + + "github.com/etcd-io/auger/pkg/client" + "github.com/etcd-io/auger/pkg/encoding" + "github.com/etcd-io/auger/pkg/scheme" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type getFlagpole struct { + Namespace string + Output string + ChunkSize int64 + Watch bool + WatchOnly bool + Prefix string +} + +func newCtlGetCommand() *cobra.Command { + flags := &getFlagpole{} + + cmd := &cobra.Command{ + Args: cobra.RangeArgs(0, 2), + Use: "get [resource] [name]", + Short: "Gets the resource of k8s in etcd", + RunE: func(cmd *cobra.Command, args []string) error { + etcdclient, err := clientFromCmd(cmd) + if err != nil { + return err + } + err = getCommand(cmd.Context(), etcdclient, flags, args) + + if err != nil { + return fmt.Errorf("%v: %w", args, err) + } + return nil + }, + } + + cmd.Flags().StringVarP(&flags.Output, "output", "o", "yaml", "output format. One of: (json, yaml, raw, key).") + cmd.Flags().StringVarP(&flags.Namespace, "namespace", "n", "", "namespace of resource") + cmd.Flags().BoolVarP(&flags.Watch, "watch", "w", false, "after listing/getting the requested object, watch for changes") + cmd.Flags().BoolVar(&flags.WatchOnly, "watch-only", false, "watch for changes to the requested object(s), without listing/getting first") + cmd.Flags().Int64Var(&flags.ChunkSize, "chunk-size", 500, "chunk size of the list pager") + cmd.Flags().StringVar(&flags.Prefix, "prefix", "/registry", "prefix to prepend to the resource") + + return cmd +} + +func getCommand(ctx context.Context, etcdclient client.Client, flags *getFlagpole, args []string) error { + var targetGr schema.GroupResource + var targetName string + var targetNamespace string + if len(args) != 0 { + // TODO: Support get information from CRD and scheme.Codecs + // Support short name + // Check for namespaced + + gr := schema.ParseGroupResource(args[0]) + if gr.Empty() { + return fmt.Errorf("invalid resource %q", args[0]) + } + targetGr = gr + targetNamespace = flags.Namespace + if len(args) >= 2 { + targetName = args[1] + } + } + + var count int + var response func(kv *client.KeyValue) error + + switch flags.Output { + case "json": + outMediaType := encoding.JsonMediaType + response = func(kv *client.KeyValue) error { + count++ + value := kv.Value + if value == nil { + value = kv.PrevValue + } + inMediaType, _, err := encoding.DetectAndExtract(value) + if err != nil { + fmt.Fprintf(os.Stdout, "---\n# %s | raw | %v\n# %s\n", kv.Key, err, value) + return nil + } + data, _, err := encoding.Convert(scheme.Codecs, inMediaType, outMediaType, value) + if err != nil { + fmt.Fprintf(os.Stdout, "---\n# %s | raw | %v\n# %s\n", kv.Key, err, value) + } else { + fmt.Fprintf(os.Stdout, "---\n# %s | %s\n%s\n", kv.Key, inMediaType, data) + } + return nil + } + case "yaml": + outMediaType := encoding.YamlMediaType + response = func(kv *client.KeyValue) error { + count++ + value := kv.Value + if value == nil { + value = kv.PrevValue + } + inMediaType, _, err := encoding.DetectAndExtract(value) + if err != nil { + fmt.Fprintf(os.Stdout, "---\n# %s | raw | %v\n# %s\n", kv.Key, err, value) + return nil + } + data, _, err := encoding.Convert(scheme.Codecs, inMediaType, outMediaType, value) + if err != nil { + fmt.Fprintf(os.Stdout, "---\n# %s | raw | %v\n# %s\n", kv.Key, err, value) + } else { + fmt.Fprintf(os.Stdout, "---\n# %s | %s\n%s\n", kv.Key, inMediaType, data) + } + return nil + } + case "raw": + response = func(kv *client.KeyValue) error { + count++ + fmt.Fprintf(os.Stdout, "%s\n%s\n", kv.Key, kv.Value) + return nil + } + case "key": + response = func(kv *client.KeyValue) error { + count++ + fmt.Fprintf(os.Stdout, "%s\n", kv.Key) + return nil + } + default: + return fmt.Errorf("unsupported output format: %s", flags.Output) + } + + opOpts := []client.OpOption{ + client.WithName(targetName, targetNamespace), + client.WithGR(targetGr), + client.WithPageLimit(flags.ChunkSize), + client.WithResponse(response), + } + + if flags.Output == "key" { + opOpts = append(opOpts, + client.WithKeysOnly(), + ) + } + + var err error + if flags.Watch { + var rev int64 + if !flags.WatchOnly { + rev, err = etcdclient.Get(ctx, flags.Prefix, + opOpts..., + ) + if err != nil { + return err + } + } + + opOpts = append(opOpts, client.WithRevision(rev)) + + err = etcdclient.Watch(ctx, flags.Prefix, + opOpts..., + ) + if err != nil { + return err + } + } else { + _, err = etcdclient.Get(ctx, flags.Prefix, + opOpts..., + ) + if err != nil { + return err + } + + if flags.Output == "key" { + fmt.Fprintf(os.Stderr, "get %d keys\n", count) + } + } + return nil +} diff --git a/pkg/cmd/ctl/global.go b/pkg/cmd/ctl/global.go new file mode 100644 index 0000000..2a0b9c2 --- /dev/null +++ b/pkg/cmd/ctl/global.go @@ -0,0 +1,337 @@ +/* +Copyright 2024 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. +*/ + +// Copy from https://github.com/etcd-io/etcd/blob/main/etcdctl/ctlv3/command/global.go +// and made some change + +package ctl + +import ( + "crypto/tls" + "errors" + "fmt" + "os" + "strings" + "time" + + "github.com/bgentry/speakeasy" + "github.com/etcd-io/auger/pkg/client" + "github.com/spf13/cobra" + + "go.etcd.io/etcd/client/pkg/v3/srv" + "go.etcd.io/etcd/client/pkg/v3/transport" + clientv3 "go.etcd.io/etcd/client/v3" +) + +type secureCfg struct { + cert string + key string + cacert string + serverName string + + insecureTransport bool + insecureSkipVerify bool +} + +type authCfg struct { + username string + password string +} + +type discoveryCfg struct { + domain string + insecure bool + serviceName string +} + +type clientConfig struct { + endpoints []string + dialTimeout time.Duration + keepAliveTime time.Duration + keepAliveTimeout time.Duration + scfg *secureCfg + acfg *authCfg +} + +func clientConfigFromCmd(cmd *cobra.Command) (*clientConfig, error) { + var err error + cfg := &clientConfig{} + cfg.endpoints, err = endpointsFromCmd(cmd) + if err != nil { + return nil, err + } + + cfg.dialTimeout, err = cmd.Flags().GetDuration("dial-timeout") + if err != nil { + return nil, err + } + cfg.keepAliveTime, err = cmd.Flags().GetDuration("keepalive-time") + if err != nil { + return nil, err + } + cfg.keepAliveTimeout, err = cmd.Flags().GetDuration("keepalive-timeout") + if err != nil { + return nil, err + } + cfg.scfg, err = secureCfgFromCmd(cmd) + if err != nil { + return nil, err + } + cfg.acfg, err = authCfgFromCmd(cmd) + if err != nil { + return nil, err + } + return cfg, nil +} + +func clientFromCmd(cmd *cobra.Command) (client.Client, error) { + cfg, err := clientConfigFromCmd(cmd) + if err != nil { + return nil, err + } + return cfg.client() +} + +func (cc *clientConfig) client() (client.Client, error) { + cfg, err := newClientCfg(cc.endpoints, cc.dialTimeout, cc.keepAliveTime, cc.keepAliveTimeout, cc.scfg, cc.acfg) + if err != nil { + return nil, err + } + + return client.NewClient(*cfg) +} + +func newClientCfg(endpoints []string, dialTimeout, keepAliveTime, keepAliveTimeout time.Duration, scfg *secureCfg, acfg *authCfg) (*clientv3.Config, error) { + // set tls if any one tls option set + var cfgtls *transport.TLSInfo + tlsinfo := transport.TLSInfo{} + if scfg.cert != "" { + tlsinfo.CertFile = scfg.cert + cfgtls = &tlsinfo + } + + if scfg.key != "" { + tlsinfo.KeyFile = scfg.key + cfgtls = &tlsinfo + } + + if scfg.cacert != "" { + tlsinfo.TrustedCAFile = scfg.cacert + cfgtls = &tlsinfo + } + + if scfg.serverName != "" { + tlsinfo.ServerName = scfg.serverName + cfgtls = &tlsinfo + } + + cfg := &clientv3.Config{ + Endpoints: endpoints, + DialTimeout: dialTimeout, + DialKeepAliveTime: keepAliveTime, + DialKeepAliveTimeout: keepAliveTimeout, + } + + if cfgtls != nil { + clientTLS, err := cfgtls.ClientConfig() + if err != nil { + return nil, err + } + cfg.TLS = clientTLS + } + + // if key/cert is not given but user wants secure connection, we + // should still setup an empty tls configuration for gRPC to setup + // secure connection. + if cfg.TLS == nil && !scfg.insecureTransport { + cfg.TLS = &tls.Config{} + } + + // If the user wants to skip TLS verification then we should set + // the InsecureSkipVerify flag in tls configuration. + if scfg.insecureSkipVerify && cfg.TLS != nil { + cfg.TLS.InsecureSkipVerify = true + } + + if acfg != nil { + cfg.Username = acfg.username + cfg.Password = acfg.password + } + + return cfg, nil +} + +func secureCfgFromCmd(cmd *cobra.Command) (*secureCfg, error) { + cert, key, cacert, err := keyAndCertFromCmd(cmd) + if err != nil { + return nil, err + } + insecureTr, err := cmd.Flags().GetBool("insecure-transport") + if err != nil { + return nil, err + } + skipVerify, err := cmd.Flags().GetBool("insecure-skip-tls-verify") + if err != nil { + return nil, err + } + discoveryCfg, err := discoveryCfgFromCmd(cmd) + if err != nil { + return nil, err + } + + if discoveryCfg.insecure { + discoveryCfg.domain = "" + } + + return &secureCfg{ + cert: cert, + key: key, + cacert: cacert, + serverName: discoveryCfg.domain, + + insecureTransport: insecureTr, + insecureSkipVerify: skipVerify, + }, nil +} + +func keyAndCertFromCmd(cmd *cobra.Command) (cert, key, cacert string, err error) { + if cert, err = cmd.Flags().GetString("cert"); err != nil { + return "", "", "", err + } + if cert == "" && cmd.Flags().Changed("cert") { + return "", "", "", errors.New("empty string is passed to --cert option") + } + + if key, err = cmd.Flags().GetString("key"); err != nil { + return "", "", "", err + } + if key == "" && cmd.Flags().Changed("key") { + return "", "", "", errors.New("empty string is passed to --key option") + } + + if cacert, err = cmd.Flags().GetString("cacert"); err != nil { + return "", "", "", err + } + if cacert == "" && cmd.Flags().Changed("cacert") { + return "", "", "", errors.New("empty string is passed to --cacert option") + } + + return cert, key, cacert, nil +} + +func authCfgFromCmd(cmd *cobra.Command) (*authCfg, error) { + userFlag, err := cmd.Flags().GetString("user") + if err != nil { + return nil, err + } + passwordFlag, err := cmd.Flags().GetString("password") + if err != nil { + return nil, err + } + + if userFlag == "" { + return nil, nil + } + + var cfg authCfg + + if passwordFlag == "" { + splitted := strings.SplitN(userFlag, ":", 2) + if len(splitted) < 2 { + cfg.username = userFlag + cfg.password, err = speakeasy.Ask("Password: ") + if err != nil { + return nil, err + } + } else { + cfg.username = splitted[0] + cfg.password = splitted[1] + } + } else { + cfg.username = userFlag + cfg.password = passwordFlag + } + + return &cfg, nil +} + +func discoveryCfgFromCmd(cmd *cobra.Command) (*discoveryCfg, error) { + domain, err := cmd.Flags().GetString("discovery-srv") + if err != nil { + return nil, err + } + insecure, err := cmd.Flags().GetBool("insecure-discovery") + if err != nil { + return nil, err + } + serviceName, err := cmd.Flags().GetString("discovery-srv-name") + if err != nil { + return nil, err + } + return &discoveryCfg{ + domain: domain, + insecure: insecure, + serviceName: serviceName, + }, nil +} + +func endpointsFromCmd(cmd *cobra.Command) ([]string, error) { + eps, err := endpointsFromFlagValue(cmd) + if err != nil { + return nil, err + } + // If domain discovery returns no endpoints, check endpoints flag + if len(eps) == 0 { + eps, err = cmd.Flags().GetStringSlice("endpoints") + if err == nil { + for i, ip := range eps { + eps[i] = strings.TrimSpace(ip) + } + } + } + return eps, err +} + +func endpointsFromFlagValue(cmd *cobra.Command) ([]string, error) { + discoveryCfg, err := discoveryCfgFromCmd(cmd) + if err != nil { + return nil, err + } + + // If we still don't have domain discovery, return nothing + if discoveryCfg.domain == "" { + return []string{}, nil + } + + srvs, err := srv.GetClient("etcd-client", discoveryCfg.domain, discoveryCfg.serviceName) + if err != nil { + return nil, err + } + eps := srvs.Endpoints + if discoveryCfg.insecure { + return eps, err + } + // strip insecure connections + ret := []string{} + for _, ep := range eps { + if strings.HasPrefix(ep, "http://") { + fmt.Fprintf(os.Stderr, "ignoring discovered insecure endpoint %q\n", ep) + continue + } + ret = append(ret, ep) + } + return ret, err +} diff --git a/pkg/cmd/ctl/put.go b/pkg/cmd/ctl/put.go new file mode 100644 index 0000000..d8c9dd9 --- /dev/null +++ b/pkg/cmd/ctl/put.go @@ -0,0 +1,239 @@ +/* +Copyright 2024 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 ctl + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "time" + + "github.com/etcd-io/auger/pkg/client" + "github.com/etcd-io/auger/pkg/encoding" + "github.com/etcd-io/auger/pkg/scheme" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/yaml" +) + +type putFlagpole struct { + Namespace string + Output string + Path string + Prefix string +} + +func newCtlPutCommand() *cobra.Command { + flags := &putFlagpole{} + + cmd := &cobra.Command{ + Args: cobra.RangeArgs(0, 2), + Use: "put [resource] [name]", + Short: "Puts the resource of k8s in etcd", + RunE: func(cmd *cobra.Command, args []string) error { + etcdclient, err := clientFromCmd(cmd) + if err != nil { + return err + } + err = putCommand(cmd.Context(), etcdclient, flags, args) + + if err != nil { + return fmt.Errorf("%v: %w", args, err) + } + return nil + }, + } + + cmd.Flags().StringVarP(&flags.Output, "output", "o", "key", "output format. One of: (key, none).") + cmd.Flags().StringVarP(&flags.Namespace, "namespace", "n", "", "namespace of resource") + cmd.Flags().StringVar(&flags.Prefix, "prefix", "/registry", "prefix to prepend to the resource") + cmd.Flags().StringVar(&flags.Path, "path", "", "path of the file") + return cmd +} + +func putCommand(ctx context.Context, etcdclient client.Client, flags *putFlagpole, args []string) error { + var reader io.Reader + var err error + switch flags.Path { + case "-": + reader = os.Stdin + case "": + return fmt.Errorf("path is required") + default: + reader, err = os.Open(flags.Path) + if err != nil { + return err + } + } + + var wantGr *schema.GroupResource + var wantName string + if len(args) != 0 { + // TODO: Support get information from CRD and scheme.Codecs + // Support short name + // Check for namespaced + + gr := schema.ParseGroupResource(args[0]) + if gr.Empty() { + return fmt.Errorf("invalid resource %q", args[0]) + } + wantGr = &gr + if len(args) >= 2 { + wantName = args[1] + } + } + + start := time.Now() + + var count int + var response func(kv *client.KeyValue) error + if flags.Output == "key" { + //nolint:unparam + response = func(kv *client.KeyValue) error { + count++ + if kv != nil { + fmt.Fprintf(os.Stdout, "%s\n", kv.Key) + } + return nil + } + } + + err = decodeToUnstructured(reader, func(obj *unstructured.Unstructured) error { + targetName := obj.GetName() + if targetName == "" { + // There will be some unnamed hidden resources, which we should also ignore. + return nil + } + + // TODO: Use a safe way to convert GVK to GVR + // Verify that all built-in resources conform to this rule + // For custom resources try to get information from the CRD + targetGvr, _ := meta.UnsafeGuessKindToResource(obj.GroupVersionKind()) + + targetGr := targetGvr.GroupResource() + targetNamespace := obj.GetNamespace() + + if targetNamespace != "" && flags.Namespace != "" && targetNamespace != flags.Namespace { + return nil + } + + if wantGr != nil && *wantGr != targetGr { + return nil + } + + if wantName != "" && wantName != targetName { + return nil + } + + if targetName == "" { + return nil + } + + mediaType, err := client.MediaTypeFromGR(targetGr) + if err != nil { + return err + } + + t := obj.GetCreationTimestamp() + if t.IsZero() { + obj.SetCreationTimestamp(metav1.Time{Time: start}) + } + + obj.SetResourceVersion("") + obj.SetSelfLink("") + + data, err := obj.MarshalJSON() + if err != nil { + return err + } + + data, _, err = encoding.Convert(scheme.Codecs, encoding.JsonMediaType, mediaType, data) + if err != nil { + return err + } + + opOpts := []client.OpOption{ + client.WithName(targetName, targetNamespace), + client.WithGR(targetGr), + } + + if response != nil { + opOpts = append(opOpts, + client.WithResponse(response), + client.WithKeysOnly(), + ) + } + + err = etcdclient.Put(ctx, flags.Prefix, data, + opOpts..., + ) + if err != nil { + return err + } + return nil + }) + if err != nil { + return err + } + + if flags.Output == "key" { + fmt.Fprintf(os.Stderr, "put %d keys\n", count) + } + return nil +} + +func decodeToUnstructured(reader io.Reader, visitFunc func(obj *unstructured.Unstructured) error) error { + d := yaml.NewYAMLToJSONDecoder(reader) + + for { + obj := &unstructured.Unstructured{} + err := d.Decode(&obj) + if err != nil { + if errors.Is(err, io.EOF) { + return nil + } + return err + } + + if obj.IsList() { + err = obj.EachListItem(func(object runtime.Object) error { + obj := object.(*unstructured.Unstructured) + if len(obj.Object) == 0 { + return nil + } + return visitFunc(object.(*unstructured.Unstructured)) + }) + if err != nil { + return err + } + } else { + if len(obj.Object) == 0 { + continue + } + err = visitFunc(obj) + if err != nil { + return err + } + } + } +}