diff --git a/cmd/kubectl-directpv/install.go b/cmd/kubectl-directpv/install.go index 0c482d491..78ec556bc 100644 --- a/cmd/kubectl-directpv/install.go +++ b/cmd/kubectl-directpv/install.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "os" + "path" "github.com/fatih/color" "github.com/minio/directpv/pkg/consts" @@ -48,6 +49,7 @@ var ( apparmorProfile = "" auditInstall = "install" imagePullSecrets = []string{} + configFile = path.Join(os.Getenv("HOME"), consts.ConfigFileSuffix) ) func init() { @@ -62,6 +64,7 @@ func init() { installCmd.PersistentFlags().StringSliceVarP(&tolerationParameters, "tolerations", "t", tolerationParameters, "Tolerations parameters") installCmd.PersistentFlags().StringVarP(&seccompProfile, "seccomp-profile", "", seccompProfile, "Set Seccomp profile") installCmd.PersistentFlags().StringVarP(&apparmorProfile, "apparmor-profile", "", apparmorProfile, "Set Apparmor profile") + installCmd.PersistentFlags().StringVarP(&configFile, fmt.Sprintf("%s-config", consts.AppName), "", configFile, fmt.Sprintf("Specify %s config file path", consts.AppPrettyName)) } func install(ctx context.Context, args []string) (err error) { @@ -100,6 +103,7 @@ func install(ctx context.Context, args []string) (err error) { DryRun: dryRun, AuditFile: file, ImagePullSecrets: imagePullSecrets, + ConfigFile: configFile, } if err = installer.Install(ctx, installConfig); err == nil && !dryRun { diff --git a/go.mod b/go.mod index 76827d14e..09a103640 100644 --- a/go.mod +++ b/go.mod @@ -4,22 +4,20 @@ go 1.17 require ( github.com/container-storage-interface/spec v1.3.0 - github.com/docker/distribution v2.8.1+incompatible github.com/dustin/go-humanize v1.0.0 github.com/fatih/color v1.12.0 - github.com/google/uuid v1.1.2 + github.com/google/uuid v1.3.0 + github.com/hashicorp/errwrap v1.1.0 github.com/jedib0t/go-pretty v4.3.0+incompatible - github.com/jedib0t/go-pretty/v6 v6.0.5 + github.com/jedib0t/go-pretty/v6 v6.3.9 github.com/kubernetes-csi/csi-lib-utils v0.7.0 - github.com/mb0/glob v0.0.0-20160210091149-1eb79d2de6c4 + github.com/minio/minio-go/v7 v7.0.36 github.com/minio/sha256-simd v1.0.0 github.com/mitchellh/go-homedir v1.1.0 - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 github.com/prometheus/client_golang v1.12.2 github.com/prometheus/client_model v0.2.0 github.com/spf13/cobra v1.4.0 github.com/spf13/viper v1.8.1 - go.uber.org/multierr v1.6.0 golang.org/x/sys v0.0.0-20220913175220-63ea55921009 golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 google.golang.org/grpc v1.40.0 @@ -28,7 +26,6 @@ require ( k8s.io/apiextensions-apiserver v0.24.3 k8s.io/apimachinery v0.24.3 k8s.io/client-go v0.24.3 - k8s.io/klog v1.0.0 k8s.io/klog/v2 v2.60.1 k8s.io/kube-openapi v0.0.0-20220627174259-011e075b9cb8 sigs.k8s.io/yaml v1.2.0 @@ -65,19 +62,17 @@ require ( github.com/google/gnostic v0.5.7-v3refs // indirect github.com/google/go-cmp v0.5.6 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/gopherjs/gopherjs v0.0.0-20190328170749-bb2674552d8f // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/imdario/mergo v0.3.7 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.0.4 // indirect + github.com/klauspost/cpuid/v2 v2.1.0 // indirect github.com/magiconair/properties v1.8.5 // indirect github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-colorable v0.1.8 // indirect github.com/mattn/go-isatty v0.0.13 // indirect - github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -88,30 +83,26 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.7.3 // indirect - github.com/smartystreets/assertions v1.1.1 // indirect + github.com/rivo/uniseg v0.2.0 // indirect github.com/spf13/afero v1.6.0 // indirect github.com/spf13/cast v1.3.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.2.0 // indirect - github.com/yuin/goldmark v1.4.14 // indirect go.mongodb.org/mongo-driver v1.9.1 // indirect - go.uber.org/atomic v1.7.0 // indirect - golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect - golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect + golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect golang.org/x/net v0.0.0-20220909164309-bea034e7d591 // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.7 // indirect - golang.org/x/tools v0.1.12 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368 // indirect google.golang.org/protobuf v1.27.1 // indirect gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/ini.v1 v1.62.0 // indirect + gopkg.in/ini.v1 v1.66.6 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect diff --git a/go.sum b/go.sum index c64c8f204..d5e592136 100644 --- a/go.sum +++ b/go.sum @@ -291,16 +291,15 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gopherjs/gopherjs v0.0.0-20190328170749-bb2674552d8f h1:4Gslotqbs16iAg+1KR/XdabIfq8TlAWHdwS5QJFksLc= -github.com/gopherjs/gopherjs v0.0.0-20190328170749-bb2674552d8f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= @@ -342,8 +341,8 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= -github.com/jedib0t/go-pretty/v6 v6.0.5 h1:oOo0/jSb3NEYKT6l1hhFXoX2UZnkanMuCE2DVT1mqnE= -github.com/jedib0t/go-pretty/v6 v6.0.5/go.mod h1:MTr6FgcfNdnN5wPVBzJ6mhJeDyiF0yBvS2TMXEV/XSU= +github.com/jedib0t/go-pretty/v6 v6.3.9 h1:GAK/1WJY9WVVrKd601HGB89ihLBDfJnUIJye31PY+uk= +github.com/jedib0t/go-pretty/v6 v6.3.9/go.mod h1:MgmISkTWDSFu0xOqiZ0mKNntMQ2mDgOcwOkwBEkMDJI= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -358,7 +357,6 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= @@ -367,8 +365,11 @@ github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQL github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/cpuid/v2 v2.0.4 h1:g0I61F2K2DjRHz1cnxlkNSBIaePVoJIjjnHui8QHbiw= +github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0= +github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= @@ -396,12 +397,15 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.36 h1:KPzAl8C6jcRFEUsGUHR6deRivvKATPNZThzi7D9y/sc= +github.com/minio/minio-go/v7 v7.0.36/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw= github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= @@ -466,7 +470,7 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE 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/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -502,9 +506,12 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= @@ -515,10 +522,8 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/assertions v1.1.1 h1:T/YLemO5Yp7KPzS+lVtu+WsHn8yoSwTfItdAd1r3cck= -github.com/smartystreets/assertions v1.1.1/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= @@ -547,14 +552,17 @@ github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 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.7.4 h1:wZRexSlwd7ZXfKINDLsO4r7WBt3gTKONc6K/VesHvHM= +github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= @@ -572,10 +580,6 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.4.14 h1:jwww1XQfhJN7Zm+/a1ZA/3WUiEBEroYFNTiV3dKwM8U= -github.com/yuin/goldmark v1.4.14/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= @@ -630,8 +634,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -668,8 +673,6 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -721,9 +724,7 @@ golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI= golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= @@ -754,7 +755,6 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -824,10 +824,12 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/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.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220913175220-63ea55921009 h1:PuvuRMeLWqsf/ZdT1UUZz0syhioyv1mzuFZsXs4fvhw= golang.org/x/sys v0.0.0-20220913175220-63ea55921009/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= @@ -913,8 +915,6 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.10-0.20220218145154-897bd77cd717/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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= @@ -1049,8 +1049,9 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= +gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= @@ -1068,8 +1069,8 @@ 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-20200615113413-eeeca48fe776/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.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= -gopkg.in/yaml.v3 v3.0.0/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= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/consts/consts.go b/pkg/consts/consts.go index bdc77e931..d5ff548d0 100644 --- a/pkg/consts/consts.go +++ b/pkg/consts/consts.go @@ -130,4 +130,12 @@ const ( PrivateKeyFileName = "key.pem" PublicCertFileName = "cert.pem" CACertFileName = "ca.crt" + + // Credentials + CredentialsSecretName = AppName + "-creds" + AccessKeyDataKey = "accessKey" + SecretKeyDataKey = "secretKey" + ConfigFileSuffix = "." + AppName + "/config.json" + AccessKeyEnv = AppCapsName + "_ACCESS_KEY" + SecretKeyEnv = AppCapsName + "_SECRET_KEY" ) diff --git a/pkg/consts/consts.go.in b/pkg/consts/consts.go.in index cb04d329c..672ab4f57 100644 --- a/pkg/consts/consts.go.in +++ b/pkg/consts/consts.go.in @@ -128,4 +128,12 @@ const ( PrivateKeyFileName = "key.pem" PublicCertFileName = "cert.pem" CACertFileName = "ca.crt" + + // Credentials + CredentialsSecretName = AppName + "-creds" + AccessKeyDataKey = "accessKey" + SecretKeyDataKey = "secretKey" + ConfigFileSuffix = "." + AppName + "/config.json" + AccessKeyEnv = AppCapsName + "_ACCESS_KEY" + SecretKeyEnv = AppCapsName + "_SECRET_KEY" ) diff --git a/pkg/credential/credential.go b/pkg/credential/credential.go new file mode 100644 index 000000000..e8178005c --- /dev/null +++ b/pkg/credential/credential.go @@ -0,0 +1,120 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2021, 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package credential + +import ( + "context" + "encoding/json" + "errors" + "io/ioutil" + "os" + + "github.com/minio/directpv/pkg/consts" + "github.com/minio/directpv/pkg/k8s" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "k8s.io/klog/v2" +) + +var ( + errSecretKeyEnvNotSet = errors.New(consts.SecretKeyEnv + " environment variable is not set") + errAccessKeyEnvNotSet = errors.New(consts.AccessKeyEnv + " environment variable is not set") + errEnvNotFound = errors.New("no environment variable settings for credentials found") + errSecretNotFound = errors.New("credential secret " + consts.CredentialsSecretName + " not found") + errAccessKeyNotFound = errors.New("accessKey not found in the secret") + errSecretKeyNotFound = errors.New("secretKey not found in the secret") +) + +// Credential represents the access and secret key pairs for authentication +type Credential struct { + AccessKey string `json:"accessKey"` + SecretKey string `json:"secretKey"` +} + +// Load loads the credential from the ENV. If ENV settings are not present, it loads from the config file provided +func Load(configFile string) (Credential, error) { + cred, err := loadFromEnv() + if err != nil { + if err != errEnvNotFound { + return cred, err + } + } else { + return cred, err + } + return loadFromFile(configFile) +} + +// LoadFromSecret loads the credential from the k8s secret +func LoadFromSecret(ctx context.Context) (cred Credential, err error) { + credSecret, err := k8s.KubeClient().CoreV1().Secrets(consts.Namespace).Get(context.Background(), consts.CredentialsSecretName, metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + err = errSecretNotFound + return + } + return cred, err + } + + accessKey, ok := credSecret.Data[consts.AccessKeyDataKey] + if !ok { + return cred, errAccessKeyNotFound + } + cred.AccessKey = string(accessKey) + + secretKey, ok := credSecret.Data[consts.SecretKeyDataKey] + if !ok { + return cred, errSecretKeyNotFound + } + cred.SecretKey = string(secretKey) + + return cred, nil +} + +// loadFromEnv loads the credential from ENVs +func loadFromEnv() (cred Credential, err error) { + accessKey, accessKeySet := os.LookupEnv(consts.AccessKeyEnv) + secretKey, secretKeySet := os.LookupEnv(consts.SecretKeyEnv) + + switch { + case accessKeySet && secretKeySet: + cred.AccessKey = accessKey + cred.SecretKey = secretKey + case accessKeySet && !secretKeySet: + err = errSecretKeyEnvNotSet + case secretKeySet && !accessKeySet: + err = errAccessKeyEnvNotSet + case !accessKeySet && !secretKeySet: + err = errEnvNotFound + } + + return +} + +// loadFromFile loads the credential from provided config file +func loadFromFile(configFile string) (Credential, error) { + cred := Credential{} + fileData, err := ioutil.ReadFile(configFile) + if err != nil { + return cred, err + } + err = json.Unmarshal(fileData, &cred) + if err != nil { + klog.Infof("\n unable to parse the config file %s: %v", configFile, err) + } + return cred, err +} diff --git a/pkg/device/utils.go b/pkg/device/utils.go index 7b21275a6..c9694782a 100644 --- a/pkg/device/utils.go +++ b/pkg/device/utils.go @@ -167,19 +167,19 @@ func updateFSInfo(device *Device) error { udevFSType := device.FSType() if udevFSType == "" || strings.EqualFold(udevFSType, "xfs") { fsuuid, _, totalCapacity, freeCapacity, err := xfs.Probe(device.Path()) - if err != nil && device.Size > 0 { - switch { - case errors.Is(err, xfs.ErrFSNotFound), errors.Is(err, xfs.ErrCanceled), errors.Is(err, io.ErrUnexpectedEOF): - default: - return err - } - } if err != nil { - return err + if device.Size > 0 { + switch { + case errors.Is(err, xfs.ErrFSNotFound), errors.Is(err, xfs.ErrCanceled), errors.Is(err, io.ErrUnexpectedEOF): + default: + return err + } + } + } else { + device.FSUUID = fsuuid + device.TotalCapacity = totalCapacity + device.FreeCapacity = freeCapacity } - device.FSUUID = fsuuid - device.TotalCapacity = totalCapacity - device.FreeCapacity = freeCapacity } return nil } diff --git a/pkg/installer/config.go b/pkg/installer/config.go index 2a7e289c1..2255fb956 100644 --- a/pkg/installer/config.go +++ b/pkg/installer/config.go @@ -80,6 +80,9 @@ type Config struct { // Image pull secrets ImagePullSecrets []string + + // Credential + ConfigFile string } type installer interface { diff --git a/pkg/installer/default.go b/pkg/installer/default.go index 87d1b47e4..5486abb58 100644 --- a/pkg/installer/default.go +++ b/pkg/installer/default.go @@ -50,6 +50,20 @@ func (v *defaultInstaller) installNS(ctx context.Context) error { return err } +func (v *defaultInstaller) installSecrets(ctx context.Context) error { + timer := time.AfterFunc( + 3*time.Second, + func() { fmt.Fprintln(os.Stderr, color.HiYellowString("WARNING: too long to create Secrets")) }, + ) + defer timer.Stop() + + err := installSecretsDefault(ctx, v.Config) + if err != nil && !v.DryRun { + fmt.Fprintf(os.Stderr, "%v unable to create Secrets; %v", color.HiRedString("ERROR"), err) + } + return err +} + func (v *defaultInstaller) installRBAC(ctx context.Context) error { timer := time.AfterFunc( 3*time.Second, @@ -195,6 +209,20 @@ func (v *defaultInstaller) uninstallNS(ctx context.Context) error { return err } +func (v *defaultInstaller) uninstallSecrets(ctx context.Context) error { + timer := time.AfterFunc( + 3*time.Second, + func() { fmt.Fprintln(os.Stderr, color.HiYellowString("WARNING: too long to delete Secrets")) }, + ) + defer timer.Stop() + + err := uninstallSecretsDefault(ctx, v.Config) + if err != nil && !v.DryRun { + fmt.Fprintf(os.Stderr, "%v unable to delete Secrets; %v", color.HiRedString("ERROR"), err) + } + return err +} + func (v *defaultInstaller) uninstallRBAC(ctx context.Context) error { timer := time.AfterFunc( 3*time.Second, @@ -329,6 +357,9 @@ func (v *defaultInstaller) Install(ctx context.Context) error { if err := v.installNS(ctx); err != nil { return err } + if err := v.installSecrets(ctx); err != nil { + return err + } if err := v.installRBAC(ctx); err != nil { return err } @@ -384,5 +415,8 @@ func (v *defaultInstaller) Uninstall(ctx context.Context) error { if err := v.uninstallRBAC(ctx); err != nil { return err } + if err := v.uninstallSecrets(ctx); err != nil { + return err + } return v.uninstallNS(ctx) } diff --git a/pkg/installer/rbac.go b/pkg/installer/rbac.go index 009b11d80..37e6fd7a7 100644 --- a/pkg/installer/rbac.go +++ b/pkg/installer/rbac.go @@ -312,6 +312,20 @@ func createClusterRole(ctx context.Context, c *Config) error { "", }, }, + { + Verbs: []string{ + clusterRoleVerbGet, + clusterRoleVerbList, + clusterRoleVerbWatch, + }, + Resources: []string{ + "secrets", + "secret", + }, + APIGroups: []string{ + "", + }, + }, }, AggregationRule: nil, } diff --git a/pkg/installer/secrets.go b/pkg/installer/secrets.go new file mode 100644 index 000000000..cde430729 --- /dev/null +++ b/pkg/installer/secrets.go @@ -0,0 +1,67 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2021, 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package installer + +import ( + "context" + "errors" + "fmt" + "os" + + "github.com/minio/directpv/pkg/consts" + "github.com/minio/directpv/pkg/credential" + "github.com/minio/directpv/pkg/k8s" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func installSecretsDefault(ctx context.Context, c *Config) error { + if err := installCredSecret(ctx, c); err != nil { + return err + } + // Add more secrets here.. + return nil +} + +func uninstallSecretsDefault(ctx context.Context, c *Config) error { + if err := uninstallCredSecret(ctx, c); err != nil { + return err + } + // Delete more secrets here + return nil +} + +func installCredSecret(ctx context.Context, c *Config) error { + cred, err := credential.Load(c.ConfigFile) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + err = fmt.Errorf("credentials not found: please configure access and secret keys via ENV or config file (%s)", c.ConfigFile) + } + return err + } + return createOrUpdateSecret(ctx, consts.CredentialsSecretName, map[string][]byte{ + consts.AccessKeyDataKey: []byte(cred.AccessKey), + consts.SecretKeyDataKey: []byte(cred.SecretKey), + }, c) +} + +func uninstallCredSecret(ctx context.Context, c *Config) error { + if err := k8s.KubeClient().CoreV1().Secrets(c.namespace()).Delete(ctx, consts.CredentialsSecretName, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) { + return err + } + return nil +} diff --git a/pkg/installer/v1dot18.go b/pkg/installer/v1dot18.go index 96192639f..0e26f8feb 100644 --- a/pkg/installer/v1dot18.go +++ b/pkg/installer/v1dot18.go @@ -33,6 +33,10 @@ func (v *v1dot18) installNS(ctx context.Context) error { return installNSDefault(ctx, v.Config) } +func (v *v1dot18) installSecrets(ctx context.Context) error { + return installSecretsDefault(ctx, v.Config) +} + func (v *v1dot18) installRBAC(ctx context.Context) error { return installRBACDefault(ctx, v.Config) } @@ -74,6 +78,10 @@ func (v *v1dot18) uninstallNS(ctx context.Context) error { return uninstallNSDefault(ctx, v.Config) } +func (v *v1dot18) uninstallSecrets(ctx context.Context) error { + return uninstallSecretsDefault(ctx, v.Config) +} + func (v *v1dot18) uninstallRBAC(ctx context.Context) error { return uninstallRBACDefault(ctx, v.Config) } @@ -114,6 +122,9 @@ func (v *v1dot18) Install(ctx context.Context) error { if err := v.installNS(ctx); err != nil { return err } + if err := v.installSecrets(ctx); err != nil { + return err + } if err := v.installRBAC(ctx); err != nil { return err } @@ -169,5 +180,8 @@ func (v *v1dot18) Uninstall(ctx context.Context) error { if err := v.uninstallRBAC(ctx); err != nil { return err } + if err := v.uninstallSecrets(ctx); err != nil { + return err + } return v.uninstallNS(ctx) } diff --git a/pkg/installer/v1dot19.go b/pkg/installer/v1dot19.go index 74c69706a..608f0a50b 100644 --- a/pkg/installer/v1dot19.go +++ b/pkg/installer/v1dot19.go @@ -33,6 +33,10 @@ func (v *v1dot19) installNS(ctx context.Context) error { return installNSDefault(ctx, v.Config) } +func (v *v1dot19) installSecrets(ctx context.Context) error { + return installSecretsDefault(ctx, v.Config) +} + func (v *v1dot19) installRBAC(ctx context.Context) error { return installRBACDefault(ctx, v.Config) } @@ -74,6 +78,10 @@ func (v *v1dot19) uninstallNS(ctx context.Context) error { return uninstallNSDefault(ctx, v.Config) } +func (v *v1dot19) uninstallSecrets(ctx context.Context) error { + return uninstallSecretsDefault(ctx, v.Config) +} + func (v *v1dot19) uninstallRBAC(ctx context.Context) error { return uninstallRBACDefault(ctx, v.Config) } @@ -114,6 +122,9 @@ func (v *v1dot19) Install(ctx context.Context) error { if err := v.installNS(ctx); err != nil { return err } + if err := v.installSecrets(ctx); err != nil { + return err + } if err := v.installRBAC(ctx); err != nil { return err } @@ -169,5 +180,8 @@ func (v *v1dot19) Uninstall(ctx context.Context) error { if err := v.uninstallRBAC(ctx); err != nil { return err } + if err := v.uninstallSecrets(ctx); err != nil { + return err + } return v.uninstallNS(ctx) } diff --git a/pkg/installer/v1dot20.go b/pkg/installer/v1dot20.go index 9e641c8ee..100dda783 100644 --- a/pkg/installer/v1dot20.go +++ b/pkg/installer/v1dot20.go @@ -33,6 +33,10 @@ func (v *v1dot20) installNS(ctx context.Context) error { return installNSDefault(ctx, v.Config) } +func (v *v1dot20) installSecrets(ctx context.Context) error { + return installSecretsDefault(ctx, v.Config) +} + func (v *v1dot20) installRBAC(ctx context.Context) error { return installRBACDefault(ctx, v.Config) } @@ -74,6 +78,10 @@ func (v *v1dot20) uninstallNS(ctx context.Context) error { return uninstallNSDefault(ctx, v.Config) } +func (v *v1dot20) uninstallSecrets(ctx context.Context) error { + return uninstallSecretsDefault(ctx, v.Config) +} + func (v *v1dot20) uninstallRBAC(ctx context.Context) error { return uninstallRBACDefault(ctx, v.Config) } @@ -114,6 +122,9 @@ func (v *v1dot20) Install(ctx context.Context) error { if err := v.installNS(ctx); err != nil { return err } + if err := v.installSecrets(ctx); err != nil { + return err + } if err := v.installRBAC(ctx); err != nil { return err } @@ -169,5 +180,8 @@ func (v *v1dot20) Uninstall(ctx context.Context) error { if err := v.uninstallRBAC(ctx); err != nil { return err } + if err := v.uninstallSecrets(ctx); err != nil { + return err + } return v.uninstallNS(ctx) } diff --git a/pkg/installer/v1dot21.go b/pkg/installer/v1dot21.go index 533101afe..ac1d51923 100644 --- a/pkg/installer/v1dot21.go +++ b/pkg/installer/v1dot21.go @@ -33,6 +33,10 @@ func (v *v1dot21) installNS(ctx context.Context) error { return installNSDefault(ctx, v.Config) } +func (v *v1dot21) installSecrets(ctx context.Context) error { + return installSecretsDefault(ctx, v.Config) +} + func (v *v1dot21) installRBAC(ctx context.Context) error { return installRBACDefault(ctx, v.Config) } @@ -74,6 +78,10 @@ func (v *v1dot21) uninstallNS(ctx context.Context) error { return uninstallNSDefault(ctx, v.Config) } +func (v *v1dot21) uninstallSecrets(ctx context.Context) error { + return uninstallSecretsDefault(ctx, v.Config) +} + func (v *v1dot21) uninstallRBAC(ctx context.Context) error { return uninstallRBACDefault(ctx, v.Config) } @@ -114,6 +122,9 @@ func (v *v1dot21) Install(ctx context.Context) error { if err := v.installNS(ctx); err != nil { return err } + if err := v.installSecrets(ctx); err != nil { + return err + } if err := v.installRBAC(ctx); err != nil { return err } @@ -169,5 +180,8 @@ func (v *v1dot21) Uninstall(ctx context.Context) error { if err := v.uninstallRBAC(ctx); err != nil { return err } + if err := v.uninstallSecrets(ctx); err != nil { + return err + } return v.uninstallNS(ctx) } diff --git a/pkg/installer/v1dot22.go b/pkg/installer/v1dot22.go index 962318ec9..9065710bf 100644 --- a/pkg/installer/v1dot22.go +++ b/pkg/installer/v1dot22.go @@ -33,6 +33,10 @@ func (v *v1dot22) installNS(ctx context.Context) error { return installNSDefault(ctx, v.Config) } +func (v *v1dot22) installSecrets(ctx context.Context) error { + return installSecretsDefault(ctx, v.Config) +} + func (v *v1dot22) installRBAC(ctx context.Context) error { return installRBACDefault(ctx, v.Config) } @@ -74,6 +78,10 @@ func (v *v1dot22) uninstallNS(ctx context.Context) error { return uninstallNSDefault(ctx, v.Config) } +func (v *v1dot22) uninstallSecrets(ctx context.Context) error { + return uninstallSecretsDefault(ctx, v.Config) +} + func (v *v1dot22) uninstallRBAC(ctx context.Context) error { return uninstallRBACDefault(ctx, v.Config) } @@ -114,6 +122,9 @@ func (v *v1dot22) Install(ctx context.Context) error { if err := v.installNS(ctx); err != nil { return err } + if err := v.installSecrets(ctx); err != nil { + return err + } if err := v.installRBAC(ctx); err != nil { return err } @@ -169,5 +180,8 @@ func (v *v1dot22) Uninstall(ctx context.Context) error { if err := v.uninstallRBAC(ctx); err != nil { return err } + if err := v.uninstallSecrets(ctx); err != nil { + return err + } return v.uninstallNS(ctx) } diff --git a/pkg/installer/v1dot23.go b/pkg/installer/v1dot23.go index 7aa40d650..4f47a6af0 100644 --- a/pkg/installer/v1dot23.go +++ b/pkg/installer/v1dot23.go @@ -33,6 +33,10 @@ func (v *v1dot23) installNS(ctx context.Context) error { return installNSDefault(ctx, v.Config) } +func (v *v1dot23) installSecrets(ctx context.Context) error { + return installSecretsDefault(ctx, v.Config) +} + func (v *v1dot23) installRBAC(ctx context.Context) error { return installRBACDefault(ctx, v.Config) } @@ -74,6 +78,10 @@ func (v *v1dot23) uninstallNS(ctx context.Context) error { return uninstallNSDefault(ctx, v.Config) } +func (v *v1dot23) uninstallSecrets(ctx context.Context) error { + return uninstallSecretsDefault(ctx, v.Config) +} + func (v *v1dot23) uninstallRBAC(ctx context.Context) error { return uninstallRBACDefault(ctx, v.Config) } @@ -114,6 +122,9 @@ func (v *v1dot23) Install(ctx context.Context) error { if err := v.installNS(ctx); err != nil { return err } + if err := v.installSecrets(ctx); err != nil { + return err + } if err := v.installRBAC(ctx); err != nil { return err } @@ -169,5 +180,8 @@ func (v *v1dot23) Uninstall(ctx context.Context) error { if err := v.uninstallRBAC(ctx); err != nil { return err } + if err := v.uninstallSecrets(ctx); err != nil { + return err + } return v.uninstallNS(ctx) } diff --git a/pkg/rest/api-response.go b/pkg/rest/api-response.go index 12e9f5512..227e9bc4d 100644 --- a/pkg/rest/api-response.go +++ b/pkg/rest/api-response.go @@ -40,7 +40,7 @@ func writeErrorResponse(w http.ResponseWriter, statusCode int, apiErr apiError) var err error responseBytes, err = json.Marshal(apiErr) if err != nil { - klog.Errorf("couldn't marshal the apiError %v: %v", apiErr, err) + klog.Errorf("couldn't marshal the apierror %v: %v", apiErr, err) responseBytes = []byte(apiErr.Description) } writeResponse(w, statusCode, responseBytes) diff --git a/pkg/rest/api.go b/pkg/rest/api-server.go similarity index 94% rename from pkg/rest/api.go rename to pkg/rest/api-server.go index 1ca6df8ca..8c80fd7bb 100644 --- a/pkg/rest/api.go +++ b/pkg/rest/api-server.go @@ -65,8 +65,8 @@ func ServeAPIServer(ctx context.Context, apiPort int) error { // define http server and server handler mux := http.NewServeMux() - mux.HandleFunc(devicesListAPIPath, listDevicesHandler) - mux.HandleFunc(devicesFormatAPIPath, formatDrivesHandler) + mux.HandleFunc(devicesListAPIPath, authMiddleware(listDevicesHandler)) + mux.HandleFunc(devicesFormatAPIPath, authMiddleware(formatDevicesHandler)) mux.HandleFunc(consts.ReadinessPath, readinessHandler) server.Handler = mux @@ -122,19 +122,19 @@ func listDevicesHandler(w http.ResponseWriter, r *http.Request) { // listDevices queries the nodes parallelly to get the available and unavailable devices func listDevices(ctx context.Context, req GetDevicesRequest) (map[NodeName][]Device, error) { var nodes []string - var err error - if len(req.Nodes) > 0 { - nodes, err = ellipsis.Expand(string(req.Nodes)) + for _, nodeSelector := range req.Nodes { + expanded, err := ellipsis.Expand(string(nodeSelector)) if err != nil { - return nil, fmt.Errorf("couldn't expand the node selector %v: %v", req.Nodes, err) + return nil, fmt.Errorf("couldn't expand the node selector %v: %v", nodeSelector, err) } + nodes = append(nodes, expanded...) } endpointsMap, nodeAPIPort, err := getNodeEndpoints(ctx) if err != nil { return nil, fmt.Errorf("couldn't get the node endpoints: %v", err) } httpClient := &http.Client{ - Transport: getTransport(), + Transport: getDefaultTransport(true), } reqBody, err := json.Marshal(GetDevicesRequest{ Drives: req.Drives, @@ -190,8 +190,8 @@ func listDevices(ctx context.Context, req GetDevicesRequest) (map[NodeName][]Dev return devices, nil } -// formatDrivesHandler forwards the format requests to respective nodes -func formatDrivesHandler(w http.ResponseWriter, r *http.Request) { +// formatDevicesHandler forwards the format requests to respective nodes +func formatDevicesHandler(w http.ResponseWriter, r *http.Request) { data, err := io.ReadAll(r.Body) if err != nil { klog.Errorf("couldn't read the request: %v", err) @@ -229,7 +229,7 @@ func formatDrives(ctx context.Context, req FormatDevicesRequest) (map[NodeName][ return nil, fmt.Errorf("couldn't get the node endpoints: %v", err) } httpClient := &http.Client{ - Transport: getTransport(), + Transport: getDefaultTransport(true), } var wg sync.WaitGroup var formatStatus = make(map[NodeName][]FormatDeviceStatus) diff --git a/pkg/rest/auth.go b/pkg/rest/auth.go new file mode 100644 index 000000000..7041ff258 --- /dev/null +++ b/pkg/rest/auth.go @@ -0,0 +1,105 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2021, 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package rest + +import ( + "context" + "net/http" + "time" + + "github.com/minio/directpv/pkg/credential" +) + +// authMiddleware serves as a middleware handler to terminate un-authorized requests +func authMiddleware(fn func(rw http.ResponseWriter, rq *http.Request)) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + cred, err := credential.LoadFromSecret(context.Background()) + if err != nil { + writeErrorResponse(w, http.StatusNotFound, toAPIError(err, "couldn't load the credential from secret")) + return + } + if err := doesSignatureMatch(r, cred); err != nil { + writeErrorResponse(w, http.StatusUnauthorized, toAPIError(err, "couldn't verify the signature")) + return + } + // successfully authenticated + fn(w, r) + } +} + +// doesSignatureMatch verifies the s3v4 signature in the request by comparing the self-calculated s3v4 signature +func doesSignatureMatch(r *http.Request, cred credential.Credential) error { + // Copy request. + req := *r + + // Save authorization header. + v4Auth := req.Header.Get(authorizationHeader) + + // Parse signature version '4' header. + signV4Values, err := parseSignV4(v4Auth) + if err != nil { + return err + } + + // Extract all the signed headers along with its values. + extractedSignedHeaders, err := extractSignedHeaders(signV4Values.SignedHeaders, r) + if err != nil { + return err + } + + if signV4Values.Credential.accessKey != cred.AccessKey { + return errWrongAccessKey + } + + // Extract date, if not present throw error. + var date string + if date = req.Header.Get(amzDateHeaderKey); date == "" { + if date = r.Header.Get(dateHeaderKey); date == "" { + return errMissingDateHeader + } + } + + // Parse date header. + t, e := time.Parse(iso8601Format, date) + if e != nil { + return errMalformedDate + } + + // Query string. + queryStr := req.Form.Encode() + + // Get canonical request. + canonicalRequest := getCanonicalRequest(extractedSignedHeaders, queryStr, req.URL.Path, req.Method) + + // Get string to sign from canonical request. + stringToSign := getStringToSign(canonicalRequest, t, signV4Values.Credential.getScope()) + + // Get hmac signing key. + signingKey := getSigningKey(cred.SecretKey, signV4Values.Credential.scope.date, + signV4Values.Credential.scope.region, "s3") + + // Calculate signature. + newSignature := getSignature(signingKey, stringToSign) + + // Verify if signature match. + if !compareSignatureV4(newSignature, signV4Values.Signature) { + return errSignatureDoesNotMatch + } + + // Return error none. + return nil +} diff --git a/pkg/rest/client.go b/pkg/rest/client.go new file mode 100644 index 000000000..73f3c0646 --- /dev/null +++ b/pkg/rest/client.go @@ -0,0 +1,366 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2021, 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package rest + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/http/httputil" + "net/url" + "os" + "regexp" + "runtime" + "strings" + + "github.com/minio/directpv/pkg/consts" + "github.com/minio/minio-go/v7/pkg/s3utils" + "github.com/minio/minio-go/v7/pkg/signer" +) + +var ( + errEmptyResponse = errors.New("response is empty") +) + +const ( + libraryVersion = "1.0" + // User Agent follows the below style. + // DirectPV (OS; ARCH) LIB/VER APP/VER + libraryUserAgentPrefix = consts.AppPrettyName + " (" + runtime.GOOS + "; " + runtime.GOARCH + ") " + libraryUserAgent = libraryUserAgentPrefix + consts.AppName + "/" + libraryVersion +) + +type AdminClient struct { + // parsed endpoint provided by the user + endpointURL *url.URL + // Authorization keys + accessKey string + secretKey string + // Indicate whether we are using https or not + secure bool + // Needs http client to be initialized + httpClient *http.Client + + // Advanced functionality + isTraceEnabled bool + traceOutput io.Writer + // User supplied origin info + appInfo struct { + appName string + appVersion string + } +} + +// New initiates a new admin client +func New(endpoint, accessKey, secretKey string, secure bool) (*AdminClient, error) { + clnt, err := privateNew(endpoint, accessKey, secretKey, secure) + if err != nil { + return nil, err + } + return clnt, nil +} + +func privateNew(endpoint, accessKey, secretKey string, secure bool) (*AdminClient, error) { + endpointURL, err := getEndpointURL(endpoint, secure) + if err != nil { + return nil, err + } + + clnt := new(AdminClient) + + // Set the creds + clnt.accessKey = accessKey + clnt.secretKey = secretKey + + // Remember whether we are using https or not + clnt.secure = secure + + // Save endpoint URL, user agent for future uses. + clnt.endpointURL = endpointURL + + // Instantiate http client and bucket location cache. + clnt.httpClient = &http.Client{ + Transport: getDefaultTransport(secure), + } + + return clnt, nil +} + +// SetAppInfo - add application details to user agent. +func (adm *AdminClient) SetAppInfo(appName string, appVersion string) { + if appName != "" && appVersion != "" { + adm.appInfo.appName = appName + adm.appInfo.appVersion = appVersion + } +} + +// SetCustomTransport - set new custom transport. +func (adm *AdminClient) SetCustomTransport(customHTTPTransport http.RoundTripper) { + // Set this to override default transport + // ``http.DefaultTransport``. + // + // This transport is usually needed for debugging OR to add your + // own custom TLS certificates on the client transport, for custom + // CA's and certs which are not part of standard certificate + // authority follow this example :- + // + // tr := &http.Transport{ + // TLSClientConfig: &tls.Config{RootCAs: pool}, + // DisableCompression: true, + // } + // api.SetTransport(tr) + // + if adm.httpClient != nil { + adm.httpClient.Transport = customHTTPTransport + } +} + +// TraceOn - enable HTTP tracing. +func (adm *AdminClient) TraceOn(outputStream io.Writer) { + // if outputStream is nil then default to os.Stdout. + if outputStream == nil { + outputStream = os.Stdout + } + // Sets a new output stream. + adm.traceOutput = outputStream + + // Enable tracing. + adm.isTraceEnabled = true +} + +// TraceOff - disable HTTP tracing. +func (adm *AdminClient) TraceOff() { + // Disable tracing. + adm.isTraceEnabled = false +} + +// Filter out signature value from Authorization header. +func (adm AdminClient) filterSignature(req *http.Request) { + /// Signature V4 authorization header. + + // Save the original auth. + origAuth := req.Header.Get("Authorization") + // Strip out accessKeyID from: + // Credential=////aws4_request + regCred := regexp.MustCompile("Credential=([A-Z0-9]+)/") + newAuth := regCred.ReplaceAllString(origAuth, "Credential=**REDACTED**/") + + // Strip out 256-bit signature from: Signature=<256-bit signature> + regSign := regexp.MustCompile("Signature=([[0-9a-f]+)") + newAuth = regSign.ReplaceAllString(newAuth, "Signature=**REDACTED**") + + // Set a temporary redacted auth + req.Header.Set("Authorization", newAuth) +} + +// dumpHTTP - dump HTTP request and response. +func (adm AdminClient) dumpHTTP(req *http.Request, resp *http.Response) error { + // Starts http dump. + _, err := fmt.Fprintln(adm.traceOutput, "---------START-HTTP---------") + if err != nil { + return err + } + + // Filter out Signature field from Authorization header. + adm.filterSignature(req) + + // Only display request header. + reqTrace, err := httputil.DumpRequestOut(req, false) + if err != nil { + return err + } + + // Write request to trace output. + _, err = fmt.Fprint(adm.traceOutput, string(reqTrace)) + if err != nil { + return err + } + + // Only display response header. + var respTrace []byte + + // For errors we make sure to dump response body as well. + if resp.StatusCode != http.StatusOK && + resp.StatusCode != http.StatusPartialContent && + resp.StatusCode != http.StatusNoContent { + respTrace, err = httputil.DumpResponse(resp, true) + if err != nil { + return err + } + } else { + // WORKAROUND for https://github.com/golang/go/issues/13942. + // httputil.DumpResponse does not print response headers for + // all successful calls which have response ContentLength set + // to zero. Keep this workaround until the above bug is fixed. + if resp.ContentLength == 0 { + var buffer bytes.Buffer + if err = resp.Header.Write(&buffer); err != nil { + return err + } + respTrace = buffer.Bytes() + respTrace = append(respTrace, []byte("\r\n")...) + } else { + respTrace, err = httputil.DumpResponse(resp, false) + if err != nil { + return err + } + } + } + // Write response to trace output. + _, err = fmt.Fprint(adm.traceOutput, strings.TrimSuffix(string(respTrace), "\r\n")) + if err != nil { + return err + } + + // Ends the http dump. + _, err = fmt.Fprintln(adm.traceOutput, "---------END-HTTP---------") + return err +} + +// do - execute http request. +func (adm AdminClient) do(req *http.Request) (*http.Response, error) { + resp, err := adm.httpClient.Do(req) + if err != nil { + // Handle this specifically for now until future Golang versions fix this issue properly. + if urlErr, ok := err.(*url.Error); ok { + if strings.Contains(urlErr.Err.Error(), "EOF") { + return nil, &url.Error{ + Op: urlErr.Op, + URL: urlErr.URL, + Err: errors.New("Connection closed by foreign host " + urlErr.URL + ". Retry again."), + } + } + } + return nil, err + } + + // Response cannot be non-nil + if resp == nil { + return nil, errEmptyResponse + } + + // If trace is enabled, dump http request and response. + if adm.isTraceEnabled { + err = adm.dumpHTTP(req, resp) + if err != nil { + return nil, err + } + } + return resp, nil +} + +// set User agent. +func (adm AdminClient) setUserAgent(req *http.Request) { + req.Header.Set("User-Agent", libraryUserAgent) + if adm.appInfo.appName != "" && adm.appInfo.appVersion != "" { + req.Header.Set("User-Agent", libraryUserAgent+" "+adm.appInfo.appName+"/"+adm.appInfo.appVersion) + } +} + +// RequestData exposing internal data structure requestData +type RequestData struct { + CustomHeaders http.Header + QueryValues url.Values + RelPath string // URL path relative to admin API base endpoint + Content []byte +} + +// ExecuteMethod - similar to internal method executeMethod() useful +// for writing custom requests. +func (adm AdminClient) ExecuteMethod(ctx context.Context, method string, reqData RequestData) (res *http.Response, err error) { + return adm.executeMethod(ctx, method, reqData) +} + +func (adm AdminClient) executeMethod(ctx context.Context, method string, reqData RequestData) (res *http.Response, err error) { + defer func() { + if err != nil { + // close idle connections before returning, upon error. + adm.httpClient.CloseIdleConnections() + } + }() + + // Create cancel context to control 'newRetryTimer' go routine. + ctx, cancel := context.WithCancel(ctx) + + // Indicate to our routine to exit cleanly upon return. + defer cancel() + + // Instantiate a new request. + var req *http.Request + req, err = adm.newRequest(ctx, method, reqData) + if err != nil { + return nil, err + } + + // Initiate the request. + return adm.do(req) +} + +// newRequest - instantiate a new HTTP request for a given method. +func (adm AdminClient) newRequest(ctx context.Context, method string, reqData RequestData) (req *http.Request, err error) { + // If no method is supplied default to 'POST'. + if method == "" { + method = "POST" + } + + // Construct a new target URL. + targetURL, err := adm.makeTargetURL(reqData) + if err != nil { + return nil, err + } + + // Initialize a new HTTP request for the method. + req, err = http.NewRequestWithContext(ctx, method, targetURL.String(), nil) + if err != nil { + return nil, err + } + + adm.setUserAgent(req) + for k, v := range reqData.CustomHeaders { + req.Header.Set(k, v[0]) + } + if length := len(reqData.Content); length > 0 { + req.ContentLength = int64(length) + } + req.Body = ioutil.NopCloser(bytes.NewReader(reqData.Content)) + + req = signer.SignV4(*req, adm.accessKey, adm.secretKey, "", "") + return req, nil +} + +// makeTargetURL make a new target url. +func (adm AdminClient) makeTargetURL(r RequestData) (*url.URL, error) { + host := adm.endpointURL.Host + scheme := adm.endpointURL.Scheme + + // ToDo: Embed versions for a versioned API + urlStr := scheme + "://" + host + r.RelPath + + // If there are any query values, add them to the end. + if len(r.QueryValues) > 0 { + urlStr = urlStr + "?" + s3utils.QueryEncode(r.QueryValues) + } + u, err := url.Parse(urlStr) + if err != nil { + return nil, err + } + return u, nil +} diff --git a/pkg/rest/devices_format.go b/pkg/rest/devices_format.go new file mode 100644 index 000000000..9ba32a755 --- /dev/null +++ b/pkg/rest/devices_format.go @@ -0,0 +1,52 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2021, 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package rest + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" +) + +// FormatDevices - formats the requested devices and responds with results +func (adm *AdminClient) FormatDevices(ctx context.Context, req FormatDevicesRequest) (result FormatDevicesResponse, err error) { + reqBodyInBytes, err := json.Marshal(req) + if err != nil { + return result, fmt.Errorf("errror while marshalling the request: %v", err) + } + resp, err := adm.executeMethod(ctx, "", RequestData{ + RelPath: devicesFormatAPIPath, + Content: reqBodyInBytes, + }) + defer closeResponse(resp) + if err != nil { + return result, err + } + if resp.StatusCode != http.StatusOK { + return result, httpRespToErrorResponse(resp) + } + responseInBytes, err := io.ReadAll(resp.Body) + if err != nil { + return result, fmt.Errorf("failed to read response: %v", err) + } + if err := json.Unmarshal(responseInBytes, &result); err != nil { + return result, fmt.Errorf("failed to parse the response: %v", err) + } + return +} diff --git a/pkg/rest/devices_list.go b/pkg/rest/devices_list.go new file mode 100644 index 000000000..c5adb459e --- /dev/null +++ b/pkg/rest/devices_list.go @@ -0,0 +1,52 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2021, 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package rest + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" +) + +// ListDevices - lists the available and unavailable devices present in the nodes. Also honours the selectors for filtering +func (adm *AdminClient) ListDevices(ctx context.Context, req GetDevicesRequest) (result GetDevicesResponse, err error) { + reqBodyInBytes, err := json.Marshal(req) + if err != nil { + return result, fmt.Errorf("errror while marshalling the request: %v", err) + } + resp, err := adm.executeMethod(ctx, "", RequestData{ + RelPath: devicesListAPIPath, + Content: reqBodyInBytes, + }) + defer closeResponse(resp) + if err != nil { + return result, err + } + if resp.StatusCode != http.StatusOK { + return result, httpRespToErrorResponse(resp) + } + responseInBytes, err := io.ReadAll(resp.Body) + if err != nil { + return result, fmt.Errorf("failed to read response: %v", err) + } + if err := json.Unmarshal(responseInBytes, &result); err != nil { + return result, fmt.Errorf("failed to parse the response: %v", err) + } + return +} diff --git a/pkg/rest/errors.go b/pkg/rest/errors.go index b354e8857..ad86f81a8 100644 --- a/pkg/rest/errors.go +++ b/pkg/rest/errors.go @@ -16,7 +16,13 @@ package rest -import "errors" +import ( + "encoding/json" + "errors" + "io" + "io/ioutil" + "net/http" +) var ( errNoSubsetsFound = errors.New("no subsets found for the node service") @@ -33,9 +39,36 @@ type apiError struct { Message string `json:"message"` } +func (e apiError) Error() string { + return e.Description + ": " + e.Message +} + func toAPIError(err error, message string) apiError { return apiError{ Description: err.Error(), Message: message, } } + +// httpRespToErrorResponse returns a new encoded apiError structure as error. +func httpRespToErrorResponse(resp *http.Response) error { + if resp == nil || resp.Body == nil { + return errEmptyResponse + } + defer closeResponse(resp) + + // Limit to 100K + body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 100<<10)) + if err != nil { + return toAPIError(err, "failed to read server response") + } + + // Decode the json error + var errResp apiError + err = json.Unmarshal(body, &errResp) + if err != nil { + return toAPIError(err, "failed to parse server response") + } + + return errResp +} diff --git a/pkg/rest/examples/devices_format.go b/pkg/rest/examples/devices_format.go new file mode 100644 index 000000000..bf08e9a51 --- /dev/null +++ b/pkg/rest/examples/devices_format.go @@ -0,0 +1,92 @@ +//go:build ignore +// +build ignore + +// This file is part of MinIO DirectPV +// Copyright (c) 2021, 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package main + +import ( + "context" + "fmt" + "log" + + "github.com/minio/directpv/pkg/rest" +) + +func main() { + // Note: ACCESS_KEY, SECRET_KEY are dummy values, please replace them with original values. + // CAUTION: This example may format the drives. Please be careful when executing this + admClnt, err := rest.New(":40443", "ACCESS_KEY", "SECRET_KEY", true) + if err != nil { + log.Fatalln(err) + } + + // Uncomment to enable trace + // admClnt.TraceOn(nil) + + result, err := admClnt.ListDevices(context.Background(), rest.GetDevicesRequest{ + Drives: []rest.Selector{}, // supports ellipsis. eg, `sda`, `sd{a...d}`, `sda,sd{b...d}` + Nodes: []rest.Selector{}, // supports ellipsis. eg, `node-1`, `node-{1...4}`, `node-1, node-{2...4}` + Statuses: []rest.DeviceStatus{}, // possible values are rest.DeviceStatusAvailable and rest.DeviceStatusUnavailable + }) + if err != nil { + log.Fatalln(err) + } + + formatInfo := make(map[rest.NodeName][]rest.FormatDevice) + for nodeName, deviceList := range result.DeviceInfo { + var devicesToFormat []rest.FormatDevice + for _, device := range deviceList { + devicesToFormat = append(devicesToFormat, rest.FormatDevice{ + Name: device.Name, + MajorMinor: device.MajorMinor, + Force: device.Filesystem != "", + UDevData: device.UDevData, + }) + } + if len(devicesToFormat) > 0 { + formatInfo[rest.NodeName(nodeName)] = devicesToFormat + } + } + + if len(formatInfo) == 0 { + log.Fatal("no devices listed for formatting") + } + + formatResult, err := admClnt.FormatDevices(context.Background(), rest.FormatDevicesRequest{ + FormatInfo: formatInfo, + }) + if err != nil { + log.Fatalln(err) + } + + for nodeName, formatDeviceStatusList := range formatResult.DeviceInfo { + for _, formatDeviceStatus := range formatDeviceStatusList { + if formatDeviceStatus.Error != "" { + fmt.Printf("\n failed to format device: %s from node: %s due to %s. Error: %s, Suggestion: %s", + formatDeviceStatus.Name, + nodeName, + formatDeviceStatus.Message, + formatDeviceStatus.Error, + formatDeviceStatus.Suggestion, + ) + } else { + fmt.Printf("\n successfully formatted device: %s from node: %s", formatDeviceStatus.Name, nodeName) + } + } + } +} diff --git a/pkg/rest/examples/devices_list.go b/pkg/rest/examples/devices_list.go new file mode 100644 index 000000000..7ebd73278 --- /dev/null +++ b/pkg/rest/examples/devices_list.go @@ -0,0 +1,63 @@ +//go:build ignore +// +build ignore + +// This file is part of MinIO DirectPV +// Copyright (c) 2021, 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package main + +import ( + "context" + "fmt" + "log" + + "github.com/dustin/go-humanize" + "github.com/minio/directpv/pkg/rest" +) + +func main() { + // Note: ACCESS_KEY, SECRET_KEY are dummy values, please replace them with original values. + admClnt, err := rest.New(":40443", "ACCESS_KEY", "SECRET_KEY", true) + if err != nil { + log.Fatalln(err) + } + + // Uncomment to enable trace + // admClnt.TraceOn(nil) + + result, err := admClnt.ListDevices(context.Background(), rest.GetDevicesRequest{ + Drives: []rest.Selector{}, // supports ellipsis. eg, `sda`, `sd{a...d}`, `sda,sd{b...d}` + Nodes: []rest.Selector{}, // supports ellipsis. eg, `node-1`, `node-{1...4}`, `node-1, node-{2...4}` + Statuses: []rest.DeviceStatus{}, // possible values are rest.DeviceStatusAvailable and rest.DeviceStatusUnavailable + }) + if err != nil { + log.Fatalln(err) + } + + for nodeName, deviceList := range result.DeviceInfo { + fmt.Printf("\n-------------------- Devices from Node: %s -----------------------------\n", nodeName) + for _, device := range deviceList { + fmt.Printf(" Device: %s", device.Name) + fmt.Printf("\n MajorMinor: %s", device.MajorMinor) + fmt.Printf("\n Size: %s", humanize.IBytes(device.Size)) + fmt.Printf("\n Model: %s", device.Model) + fmt.Printf("\n Vendor: %s", device.Vendor) + fmt.Printf("\n Filesystem: %s", device.Filesystem) + fmt.Printf("\n Status: %s", device.Status) + fmt.Println("\n---------XX---------") + } + } +} diff --git a/pkg/rest/node-handlers.go b/pkg/rest/node-api-handlers.go similarity index 100% rename from pkg/rest/node-handlers.go rename to pkg/rest/node-api-handlers.go diff --git a/pkg/rest/node.go b/pkg/rest/node-api-server.go similarity index 98% rename from pkg/rest/node.go rename to pkg/rest/node-api-server.go index 78c194e89..3fbcd35b1 100644 --- a/pkg/rest/node.go +++ b/pkg/rest/node-api-server.go @@ -143,12 +143,12 @@ func (n *nodeAPIHandler) listLocalDevicesHandler(w http.ResponseWriter, r *http. func (n *nodeAPIHandler) listLocalDevices(ctx context.Context, req GetDevicesRequest) ([]Device, error) { var driveSelectors, statusSelectors []string - var err error - if len(req.Drives) > 0 { - driveSelectors, err = ellipsis.Expand(string(req.Drives)) + for _, driveSelector := range req.Drives { + expanded, err := ellipsis.Expand(string(driveSelector)) if err != nil { - return nil, fmt.Errorf("couldn't expand the node selector %v: %v", req.Nodes, err) + return nil, fmt.Errorf("couldn't expand the drive selector %v: %v", driveSelector, err) } + driveSelectors = append(driveSelectors, expanded...) } for _, status := range req.Statuses { statusSelectors = append(statusSelectors, string(status)) diff --git a/pkg/rest/types.go b/pkg/rest/types.go index 11d52d2cf..e33e55e32 100644 --- a/pkg/rest/types.go +++ b/pkg/rest/types.go @@ -33,8 +33,8 @@ const ( // GetDevicesRequest is the request type to fetch the devices present in the cluster type GetDevicesRequest struct { - Nodes Selector `json:"nodes,omitempty"` - Drives Selector `json:"drives,omitempty"` + Nodes []Selector `json:"nodes,omitempty"` + Drives []Selector `json:"drives,omitempty"` Statuses []DeviceStatus `json:"statuses,omitempty"` } diff --git a/pkg/rest/utils.go b/pkg/rest/utils.go index 6a6018d43..7a1f78eb8 100644 --- a/pkg/rest/utils.go +++ b/pkg/rest/utils.go @@ -20,10 +20,12 @@ import ( "crypto/tls" "encoding/json" "errors" + "fmt" "io" "io/ioutil" "net" "net/http" + "net/url" "os" "path" "strings" @@ -31,6 +33,11 @@ import ( "github.com/minio/directpv/pkg/device" "github.com/minio/directpv/pkg/types" + "github.com/minio/minio-go/v7/pkg/s3utils" +) + +var ( + errEmptyEndpointURL = errors.New("endpoint url is empty") ) type matchFn func(drive *types.Drive, device *device.Device) bool @@ -46,10 +53,7 @@ func getMatchedDevicesForDrive(drive *types.Drive, devices []*device.Device) ([] } func fsMatcher(drive *types.Drive, device *device.Device) bool { - if drive.Status.FSUUID != device.FSUUID { - return false - } - return true + return drive.Status.FSUUID == device.FSUUID } func getMatchedDevices(drive *types.Drive, devices []*device.Device, matchFn matchFn) (matchedDevices, unmatchedDevices []*device.Device) { @@ -116,31 +120,40 @@ func (s *FormatDeviceStatus) setErr(err error, message, suggestion string) { s.Suggestion = suggestion } -func getTransport() *http.Transport { - // Keep TLS config. - tlsConfig := &tls.Config{ - // Can't use SSLv3 because of POODLE and BEAST - // Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher - // Can't use TLSv1.1 because of RC4 cipher usage - MinVersion: tls.VersionTLS12, - InsecureSkipVerify: true, // FIXME: use trusted CA - } - return &http.Transport{ +func getDefaultTransport(secure bool) *http.Transport { + tr := &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ - Timeout: 30 * time.Second, + Timeout: 50 * time.Second, }).DialContext, MaxIdleConnsPerHost: 1024, - IdleConnTimeout: 30 * time.Second, + IdleConnTimeout: 50 * time.Second, ResponseHeaderTimeout: 1 * time.Minute, - TLSHandshakeTimeout: 5 * time.Second, - ExpectContinueTimeout: 5 * time.Second, - TLSClientConfig: tlsConfig, + TLSHandshakeTimeout: 10 * time.Second, + // ExpectContinueTimeout: 5 * time.Second, // Go net/http automatically unzip if content-type is // gzip disable this feature, as we are always interested // in raw stream. DisableCompression: true, } + if secure { + // Keep TLS config. + tr.TLSClientConfig = &tls.Config{ + // Can't use SSLv3 because of POODLE and BEAST + // Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher + // Can't use TLSv1.1 because of RC4 cipher usage + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: true, // FIXME: use trusted CA + } + } + return tr +} + +// Close the response properly for the RoundTripper to re-use the connections +func closeResponse(resp *http.Response) { + if resp != nil { + drainBody(resp.Body) + } } // drainBody close non nil response with any response Body. @@ -162,3 +175,54 @@ func drainBody(respBody io.ReadCloser) { io.Copy(ioutil.Discard, respBody) } } + +// getEndpointURL - construct a new endpoint. +func getEndpointURL(endpoint string, secure bool) (*url.URL, error) { + if strings.Contains(endpoint, ":") { + host, _, err := net.SplitHostPort(endpoint) + if err != nil { + return nil, err + } + if !s3utils.IsValidIP(host) && !s3utils.IsValidDomain(host) { + return nil, fmt.Errorf("endpoint: %s does not follow ip address or domain name standards", endpoint) + } + } else { + if !s3utils.IsValidIP(endpoint) && !s3utils.IsValidDomain(endpoint) { + return nil, fmt.Errorf("endpoint: %s does not follow ip address or domain name standards", endpoint) + } + } + + // If secure is false, use 'http' scheme. + scheme := "https" + if !secure { + scheme = "http" + } + + // Construct a secured endpoint URL. + endpointURLStr := scheme + "://" + endpoint + endpointURL, err := url.Parse(endpointURLStr) + if err != nil { + return nil, err + } + + // Validate incoming endpoint URL. + if err := isValidEndpointURL(endpointURL.String()); err != nil { + return nil, err + } + return endpointURL, nil +} + +// Verify if input endpoint URL is valid. +func isValidEndpointURL(endpointURL string) error { + if endpointURL == "" { + return errEmptyEndpointURL + } + url, err := url.Parse(endpointURL) + if err != nil { + return fmt.Errorf("endpoint url %s cannot be parsed", endpointURL) + } + if url.Path != "/" && url.Path != "" { + return fmt.Errorf("endpoint url %s cannot have fully qualified paths", endpointURL) + } + return nil +} diff --git a/pkg/rest/verify_sign_utils.go b/pkg/rest/verify_sign_utils.go new file mode 100644 index 000000000..2034feb90 --- /dev/null +++ b/pkg/rest/verify_sign_utils.go @@ -0,0 +1,382 @@ +// This file is part of MinIO DirectPV +// Copyright (c) 2021, 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package rest + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "crypto/subtle" + "encoding/hex" + "errors" + "net/http" + "reflect" + "sort" + "strconv" + "strings" + "time" + + "github.com/minio/minio-go/v7/pkg/s3utils" +) + +const ( + defaultSha256Cksum = "UNSIGNED-PAYLOAD" + authorizationHeader = "Authorization" + signV4Algorithm = "AWS4-HMAC-SHA256" + SlashSeparator = "/" + accessKeyMinLen = 3 + yyyymmdd = "20060102" + amzDateHeaderKey = "X-Amz-Date" + dateHeaderKey = "Date" + iso8601Format = "20060102T150405Z" + contentTypeHeaderKey = "Content-Type" + contentMD5HeaderKey = "Content-Md5" +) + +var ( + errUnsignedHeaders = errors.New("unsigned headers found in the request") + errWrongAccessKey = errors.New("wrong access key") + errAuthHeaderEmpty = errors.New("auth header value is empty") + errSignatureVersionNotSupported = errors.New("signature version is not supported") + errMissingFields = errors.New("missing fields in the request") + errMissingCredTag = errors.New("missing cred tag") + errCredMalformed = errors.New("credential tag is malformed") + errInvalidAccessKeyID = errors.New("invalid access key id") + errMalformedCredentialDate = errors.New("date in the request is malformed") + errMissingSignHeadersTag = errors.New("missing sign headers") + errMissingSignTag = errors.New("missing sign tag") + errMissingDateHeader = errors.New("missing date header") + errMalformedDate = errors.New("malformed date value in the header") + errSignatureDoesNotMatch = errors.New("signature doesn't match") +) + +// credentialHeader data type represents structured form of Credential string from authorization header. +type credentialHeader struct { + accessKey string + scope struct { + date time.Time + region string + service string + request string + } +} + +// Return scope string. +func (c credentialHeader) getScope() string { + return strings.Join([]string{ + c.scope.date.Format(yyyymmdd), + c.scope.region, + c.scope.service, + c.scope.request, + }, SlashSeparator) +} + +// signValues data type represents structured form of AWS Signature V4 header. +type signValues struct { + Credential credentialHeader + SignedHeaders []string + Signature string +} + +// extractSignedHeaders extract signed headers from Authorization header +func extractSignedHeaders(signedHeaders []string, r *http.Request) (http.Header, error) { + reqHeaders := r.Header + reqQueries := r.Form + // find whether "host" is part of list of signed headers. + // if not return ErrUnsignedHeaders. "host" is mandatory. + if !contains(signedHeaders, "host") { + return nil, errUnsignedHeaders + } + extractedSignedHeaders := make(http.Header) + for _, header := range signedHeaders { + // `host` will not be found in the headers, can be found in r.Host. + // but its alway necessary that the list of signed headers containing host in it. + val, ok := reqHeaders[http.CanonicalHeaderKey(header)] + if !ok { + // try to set headers from Query String + val, ok = reqQueries[header] + } + if ok { + extractedSignedHeaders[http.CanonicalHeaderKey(header)] = val + continue + } + switch header { + case "expect": + // Golang http server strips off 'Expect' header, if the + // client sent this as part of signed headers we need to + // handle otherwise we would see a signature mismatch. + // `aws-cli` sets this as part of signed headers. + // + // According to + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.20 + // Expect header is always of form: + // + // Expect = "Expect" ":" 1#expectation + // expectation = "100-continue" | expectation-extension + // + // So it safe to assume that '100-continue' is what would + // be sent, for the time being keep this work around. + // Adding a *TODO* to remove this later when Golang server + // doesn't filter out the 'Expect' header. + extractedSignedHeaders.Set(header, "100-continue") + case "host": + // Go http server removes "host" from Request.Header + extractedSignedHeaders.Set(header, r.Host) + case "transfer-encoding": + // Go http server removes "host" from Request.Header + extractedSignedHeaders[http.CanonicalHeaderKey(header)] = r.TransferEncoding + case "content-length": + // Signature-V4 spec excludes Content-Length from signed headers list for signature calculation. + // But some clients deviate from this rule. Hence we consider Content-Length for signature + // calculation to be compatible with such clients. + extractedSignedHeaders.Set(header, strconv.FormatInt(r.ContentLength, 10)) + default: + return nil, errUnsignedHeaders + } + } + return extractedSignedHeaders, nil +} + +func contains(slice interface{}, elem interface{}) bool { + v := reflect.ValueOf(slice) + if v.Kind() == reflect.Slice { + for i := 0; i < v.Len(); i++ { + if v.Index(i).Interface() == elem { + return true + } + } + } + return false +} + +func parseSignV4(v4Auth string) (sv signValues, err error) { + // credElement is fetched first to skip replacing the space in access key. + credElement := strings.TrimPrefix(strings.Split(strings.TrimSpace(v4Auth), ",")[0], signV4Algorithm) + // Replace all spaced strings, some clients can send spaced + // parameters and some won't. So we pro-actively remove any spaces + // to make parsing easier. + v4Auth = strings.ReplaceAll(v4Auth, " ", "") + if v4Auth == "" { + return sv, errAuthHeaderEmpty + } + + // Verify if the header algorithm is supported or not. + if !strings.HasPrefix(v4Auth, signV4Algorithm) { + return sv, errSignatureVersionNotSupported + } + + // Strip off the Algorithm prefix. + v4Auth = strings.TrimPrefix(v4Auth, signV4Algorithm) + authFields := strings.Split(strings.TrimSpace(v4Auth), ",") + if len(authFields) != 3 { + return sv, errMissingFields + } + + // Initialize signature version '4' structured header. + signV4Values := signValues{} + + // Save credentail values. + signV4Values.Credential, err = parseCredentialHeader(strings.TrimSpace(credElement)) + if err != nil { + return sv, err + } + + // Save signed headers. + signV4Values.SignedHeaders, err = parseSignedHeader(authFields[1]) + if err != nil { + return sv, err + } + + // Save signature. + signV4Values.Signature, err = parseSignature(authFields[2]) + if err != nil { + return sv, err + } + + // Return the structure here. + return signV4Values, nil +} + +// parse credentialHeader string into its structured form. +func parseCredentialHeader(credElement string) (ch credentialHeader, err error) { + creds := strings.SplitN(strings.TrimSpace(credElement), "=", 2) + if len(creds) != 2 { + return ch, errMissingFields + } + if creds[0] != "Credential" { + return ch, errMissingCredTag + } + credElements := strings.Split(strings.TrimSpace(creds[1]), SlashSeparator) + if len(credElements) < 5 { + return ch, errCredMalformed + } + accessKey := strings.Join(credElements[:len(credElements)-4], SlashSeparator) // The access key may contain one or more `/` + if len(accessKey) <= accessKeyMinLen { + return ch, errInvalidAccessKeyID + } + // Save access key id. + cred := credentialHeader{ + accessKey: accessKey, + } + credElements = credElements[len(credElements)-4:] + var e error + cred.scope.date, e = time.Parse(yyyymmdd, credElements[0]) + if e != nil { + return ch, errMalformedCredentialDate + } + cred.scope.service = credElements[2] + cred.scope.request = credElements[3] + return cred, nil +} + +// Parse slice of signed headers from signed headers tag. +func parseSignedHeader(signedHdrElement string) ([]string, error) { + signedHdrFields := strings.Split(strings.TrimSpace(signedHdrElement), "=") + if len(signedHdrFields) != 2 { + return nil, errMissingFields + } + if signedHdrFields[0] != "SignedHeaders" { + return nil, errMissingSignHeadersTag + } + if signedHdrFields[1] == "" { + return nil, errMissingFields + } + signedHeaders := strings.Split(signedHdrFields[1], ";") + return signedHeaders, nil +} + +// Parse signature from signature tag. +func parseSignature(signElement string) (string, error) { + signFields := strings.Split(strings.TrimSpace(signElement), "=") + if len(signFields) != 2 { + return "", errMissingFields + } + if signFields[0] != "Signature" { + return "", errMissingSignTag + } + if signFields[1] == "" { + return "", errMissingFields + } + signature := signFields[1] + return signature, nil +} + +// getCanonicalRequest generate a canonical request of style +// +// canonicalRequest = +// +// \n +// \n +// \n +// \n +// \n +// +func getCanonicalRequest(extractedSignedHeaders http.Header, queryStr, urlPath, method string) string { + rawQuery := strings.ReplaceAll(queryStr, "+", "%20") + encodedPath := s3utils.EncodePath(urlPath) + canonicalRequest := strings.Join([]string{ + method, + encodedPath, + rawQuery, + getCanonicalHeaders(extractedSignedHeaders), + getSignedHeaders(extractedSignedHeaders), + defaultSha256Cksum, + }, "\n") + return canonicalRequest +} + +// getCanonicalHeaders generate a list of request headers with their values +func getCanonicalHeaders(signedHeaders http.Header) string { + var headers []string + vals := make(http.Header) + for k, vv := range signedHeaders { + headers = append(headers, strings.ToLower(k)) + vals[strings.ToLower(k)] = vv + } + sort.Strings(headers) + + var buf bytes.Buffer + for _, k := range headers { + buf.WriteString(k) + buf.WriteByte(':') + for idx, v := range vals[k] { + if idx > 0 { + buf.WriteByte(',') + } + buf.WriteString(signV4TrimAll(v)) + } + buf.WriteByte('\n') + } + return buf.String() +} + +// Trim leading and trailing spaces and replace sequential spaces with one space, following Trimall() +// in http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html +func signV4TrimAll(input string) string { + // Compress adjacent spaces (a space is determined by + // unicode.IsSpace() internally here) to one space and return + return strings.Join(strings.Fields(input), " ") +} + +// getSignedHeaders generate a string i.e alphabetically sorted, semicolon-separated list of lowercase request header names +func getSignedHeaders(signedHeaders http.Header) string { + var headers []string + for k := range signedHeaders { + headers = append(headers, strings.ToLower(k)) + } + sort.Strings(headers) + return strings.Join(headers, ";") +} + +// getStringToSign a string based on selected query values. +func getStringToSign(canonicalRequest string, t time.Time, scope string) string { + stringToSign := signV4Algorithm + "\n" + t.Format(iso8601Format) + "\n" + stringToSign += scope + "\n" + canonicalRequestBytes := sha256.Sum256([]byte(canonicalRequest)) + stringToSign += hex.EncodeToString(canonicalRequestBytes[:]) + return stringToSign +} + +// getSigningKey hmac seed to calculate final signature. +func getSigningKey(secretKey string, t time.Time, region, stype string) []byte { + date := sumHMAC([]byte("AWS4"+secretKey), []byte(t.Format(yyyymmdd))) + regionBytes := sumHMAC(date, []byte(region)) + service := sumHMAC(regionBytes, []byte(stype)) + signingKey := sumHMAC(service, []byte("aws4_request")) + return signingKey +} + +// sumHMAC calculate hmac between two input byte array. +func sumHMAC(key []byte, data []byte) []byte { + hash := hmac.New(sha256.New, key) + hash.Write(data) + return hash.Sum(nil) +} + +// getSignature final signature in hexadecimal form. +func getSignature(signingKey []byte, stringToSign string) string { + return hex.EncodeToString(sumHMAC(signingKey, []byte(stringToSign))) +} + +// compareSignatureV4 returns true if and only if both signatures +// are equal. The signatures are expected to be HEX encoded strings +// according to the AWS S3 signature V4 spec. +func compareSignatureV4(sig1, sig2 string) bool { + // The CTC using []byte(str) works because the hex encoding + // is unique for a sequence of bytes. See also compareSignatureV2. + return subtle.ConstantTimeCompare([]byte(sig1), []byte(sig2)) == 1 +} diff --git a/pkg/types/aliases.go b/pkg/types/aliases.go index 685b5656f..5b9436825 100644 --- a/pkg/types/aliases.go +++ b/pkg/types/aliases.go @@ -29,6 +29,7 @@ var Versions = []string{ } var LatestAddToScheme = directpv.AddToScheme + type Interface = typeddirectpv.DirectpvV1beta1Interface type Client = typeddirectpv.DirectpvV1beta1Client