From 6cacaef6d45529db3ba180e55b3e9a9d130d1e83 Mon Sep 17 00:00:00 2001 From: Daniil Fedotov Date: Wed, 5 Jun 2024 16:25:13 -0400 Subject: [PATCH] Initial commit: move files from kanisterio/kanister --- .gitignore | 3 + args/cache_args.go | 26 ++ args/common_args.go | 23 ++ args/ephemeral_args.go | 48 ++++ doc.go | 21 ++ errors.go | 43 +++ go.mod | 25 ++ go.sum | 42 +++ internal/args/args.go | 29 ++ internal/args/args_test.go | 40 +++ internal/kopia.go | 29 ++ internal/kopia_test.go | 36 +++ internal/opts/cache_opts.go | 69 +++++ internal/opts/cache_opts_test.go | 55 ++++ internal/opts/common_opts.go | 71 +++++ internal/opts/common_opts_test.go | 61 +++++ internal/opts/opts.go | 52 ++++ internal/opts/opts_test.go | 65 +++++ internal/path.go | 28 ++ internal/path_test.go | 52 ++++ internal/test/command_suite.go | 113 ++++++++ repository/data_test.go | 80 ++++++ repository/opts.go | 123 +++++++++ repository/opts_test.go | 133 ++++++++++ repository/repository_connect.go | 57 ++++ repository/repository_connect_server.go | 55 ++++ repository/repository_connect_server_test.go | 104 ++++++++ repository/repository_connect_test.go | 131 +++++++++ repository/repository_create.go | 61 +++++ repository/repository_create_test.go | 265 +++++++++++++++++++ repository/repository_set_parameters.go | 42 +++ repository/repository_set_parameters_test.go | 88 ++++++ repository/repository_status.go | 39 +++ repository/repository_status_test.go | 66 +++++ repository/storage/azure/azure.go | 31 +++ repository/storage/azure/azure_opts.go | 39 +++ repository/storage/azure/azure_opts_test.go | 45 ++++ repository/storage/azure/azure_test.go | 59 +++++ repository/storage/fs/fs.go | 45 ++++ repository/storage/fs/fs_opts.go | 34 +++ repository/storage/fs/fs_opts_test.go | 40 +++ repository/storage/fs/fs_test.go | 53 ++++ repository/storage/gcs/gcs.go | 32 +++ repository/storage/gcs/gcs_opts.go | 47 ++++ repository/storage/gcs/gcs_opts_test.go | 55 ++++ repository/storage/gcs/gcs_test.go | 60 +++++ repository/storage/location/location.go | 36 +++ repository/storage/location/location_test.go | 82 ++++++ repository/storage/s3/s3.go | 55 ++++ repository/storage/s3/s3_opts.go | 68 +++++ repository/storage/s3/s3_opts_test.go | 65 +++++ repository/storage/s3/s3_test.go | 137 ++++++++++ 52 files changed, 3158 insertions(+) create mode 100644 args/cache_args.go create mode 100644 args/common_args.go create mode 100644 args/ephemeral_args.go create mode 100644 doc.go create mode 100644 errors.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/args/args.go create mode 100644 internal/args/args_test.go create mode 100644 internal/kopia.go create mode 100644 internal/kopia_test.go create mode 100644 internal/opts/cache_opts.go create mode 100644 internal/opts/cache_opts_test.go create mode 100644 internal/opts/common_opts.go create mode 100644 internal/opts/common_opts_test.go create mode 100644 internal/opts/opts.go create mode 100644 internal/opts/opts_test.go create mode 100644 internal/path.go create mode 100644 internal/path_test.go create mode 100644 internal/test/command_suite.go create mode 100644 repository/data_test.go create mode 100644 repository/opts.go create mode 100644 repository/opts_test.go create mode 100644 repository/repository_connect.go create mode 100644 repository/repository_connect_server.go create mode 100644 repository/repository_connect_server_test.go create mode 100644 repository/repository_connect_test.go create mode 100644 repository/repository_create.go create mode 100644 repository/repository_create_test.go create mode 100644 repository/repository_set_parameters.go create mode 100644 repository/repository_set_parameters_test.go create mode 100644 repository/repository_status.go create mode 100644 repository/repository_status_test.go create mode 100644 repository/storage/azure/azure.go create mode 100644 repository/storage/azure/azure_opts.go create mode 100644 repository/storage/azure/azure_opts_test.go create mode 100644 repository/storage/azure/azure_test.go create mode 100644 repository/storage/fs/fs.go create mode 100644 repository/storage/fs/fs_opts.go create mode 100644 repository/storage/fs/fs_opts_test.go create mode 100644 repository/storage/fs/fs_test.go create mode 100644 repository/storage/gcs/gcs.go create mode 100644 repository/storage/gcs/gcs_opts.go create mode 100644 repository/storage/gcs/gcs_opts_test.go create mode 100644 repository/storage/gcs/gcs_test.go create mode 100644 repository/storage/location/location.go create mode 100644 repository/storage/location/location_test.go create mode 100644 repository/storage/s3/s3.go create mode 100644 repository/storage/s3/s3_opts.go create mode 100644 repository/storage/s3/s3_opts_test.go create mode 100644 repository/storage/s3/s3_test.go diff --git a/.gitignore b/.gitignore index 6f6f5e6..e0bff42 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ *.dll *.so *.dylib +coverage.txt # Test binary, built with `go test -c` *.test @@ -20,3 +21,5 @@ # Go workspace file go.work go.work.sum + +/.go diff --git a/args/cache_args.go b/args/cache_args.go new file mode 100644 index 0000000..2bc77b9 --- /dev/null +++ b/args/cache_args.go @@ -0,0 +1,26 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package args + +// Cache provides the cache arguments for Kopia CLI. +type Cache struct { + CacheDirectory string // the directory where cache is stored. Default is "/tmp/kopia-cache". + ContentCacheSizeLimitMB int // the maximum size of the content cache in MB. + MetadataCacheSizeLimitMB int // the maximum size of the metadata cache in MB. + + // unused? + ContentCacheSizeMB int // the size of the content cache in MB. + MetadataCacheSizeMB int // the size of the metadata cache in MB. +} diff --git a/args/common_args.go b/args/common_args.go new file mode 100644 index 0000000..52422f5 --- /dev/null +++ b/args/common_args.go @@ -0,0 +1,23 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package args + +// Common provides the common arguments for Kopia CLI. +type Common struct { + ConfigFilePath string // the path to the config file. + LogDirectory string // the directory where logs are stored. + LogLevel string // the level of logging. Default is "error". + RepoPassword string // the password for the repository. +} diff --git a/args/ephemeral_args.go b/args/ephemeral_args.go new file mode 100644 index 0000000..86a2b0d --- /dev/null +++ b/args/ephemeral_args.go @@ -0,0 +1,48 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package args + +import ( + "fmt" + "github.com/kanisterio/safecli/command" +) + +var ( + RepositoryCreate Args + RepositoryConnectServer Args + UserAddSet Args +) + +type Args struct { + args map[string]string +} + +func (a *Args) Set(key, value string) { + if a.args == nil { + a.args = make(map[string]string) + } + if _, ok := a.args[key]; ok { + panic(fmt.Sprintf("key %q already registered", key)) + } + a.args[key] = value +} + +func (a *Args) CommandAppliers() []command.Applier { + appliers := make([]command.Applier, len(a.args)) + for k, v := range a.args { + appliers = append(appliers, command.NewOptionWithArgument(k, v)) + } + return appliers +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..75425c2 --- /dev/null +++ b/doc.go @@ -0,0 +1,21 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This package contains the implementation of the Kopia CLI using github.com/kanisterio/safecli. + +package cli + +import ( + _ "github.com/kanisterio/safecli" +) diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..6815321 --- /dev/null +++ b/errors.go @@ -0,0 +1,43 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +import ( + "github.com/pkg/errors" +) + +// Common errors +var ( + // ErrInvalidID is returned when the ID is empty. + ErrInvalidID = errors.New("invalid ID") +) + +// storage errors +var ( + // ErrUnsupportedStorage is returned when the storage is not supported. + ErrUnsupportedStorage = errors.New("unsupported storage") + // ErrInvalidRepoPath is returned when the repoPath is empty. + ErrInvalidRepoPath = errors.New("repository path cannot be empty") + // ErrInvalidPrefix is returned when the prefix is empty. + ErrInvalidPrefix = errors.New("prefix cannot be empty") + // ErrInvalidBucketName is returned when the bucketName is empty. + ErrInvalidBucketName = errors.New("bucket name cannot be empty") + // ErrInvalidCredentialsFile is returned when the credentials file is empty. + ErrInvalidCredentialsFile = errors.New("credentials file cannot be empty") + // ErrInvalidContainerName is returned when the containerName is empty. + ErrInvalidContainerName = errors.New("container name cannot be empty") + // ErrInvalidServerURL is returned when the serverURL is empty. + ErrInvalidServerURL = errors.New("server URL cannot be empty") +) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..83cc8a0 --- /dev/null +++ b/go.mod @@ -0,0 +1,25 @@ +module github.com/kanisterio/safecli-kopia + +go 1.22 + +toolchain go1.22.2 + +require ( + github.com/go-openapi/strfmt v0.23.0 + github.com/kanisterio/safecli v0.0.8 + github.com/pkg/errors v0.9.1 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c +) + +require ( + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/go-openapi/errors v0.22.0 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect + go.mongodb.org/mongo-driver v1.14.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..57de388 --- /dev/null +++ b/go.sum @@ -0,0 +1,42 @@ +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= +github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= +github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= +github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kanisterio/safecli v0.0.8 h1:flvTiGksy/a0+zvqjaBSJwxESu/nFcG65yttmR0XwiA= +github.com/kanisterio/safecli v0.0.8/go.mod h1:KBraqj8mdv2cwAr9wecknGUb8jztTzUik0r7uE6yRA8= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= +go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/args/args.go b/internal/args/args.go new file mode 100644 index 0000000..56edc46 --- /dev/null +++ b/internal/args/args.go @@ -0,0 +1,29 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package args + +import ( + "github.com/kanisterio/safecli/command" + + "github.com/kanisterio/safecli-kopia" +) + +// ID creates a new ID argument. +func ID(id string) command.Applier { + if id == "" { + return command.NewErrorArgument(cli.ErrInvalidID) + } + return command.NewArgument(id) +} diff --git a/internal/args/args_test.go b/internal/args/args_test.go new file mode 100644 index 0000000..ac7ee01 --- /dev/null +++ b/internal/args/args_test.go @@ -0,0 +1,40 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package args_test + +import ( + "testing" + + "github.com/kanisterio/safecli/test" + "gopkg.in/check.v1" + + "github.com/kanisterio/safecli-kopia" + "github.com/kanisterio/safecli-kopia/internal/args" +) + +func TestArgs(t *testing.T) { check.TestingT(t) } + +var _ = check.Suite(&test.ArgumentSuite{Cmd: "cmd", Arguments: []test.ArgumentTest{ + { + Name: "Invalid ID", + Argument: args.ID(""), + ExpectedErr: cli.ErrInvalidID, + }, + { + Name: "ID", + Argument: args.ID("id12345"), + ExpectedCLI: []string{"cmd", "id12345"}, + }, +}}) diff --git a/internal/kopia.go b/internal/kopia.go new file mode 100644 index 0000000..5af2df4 --- /dev/null +++ b/internal/kopia.go @@ -0,0 +1,29 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "github.com/kanisterio/safecli" + "github.com/kanisterio/safecli/command" +) + +const ( + kopiaBinName = "kopia" +) + +// NewKopiaCommand creates a new safecli.Builder for the kopia command. +func NewKopiaCommand(args ...command.Applier) (*safecli.Builder, error) { + return command.New(kopiaBinName, args...) +} diff --git a/internal/kopia_test.go b/internal/kopia_test.go new file mode 100644 index 0000000..b7cff47 --- /dev/null +++ b/internal/kopia_test.go @@ -0,0 +1,36 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal_test + +import ( + "testing" + + "gopkg.in/check.v1" + + "github.com/kanisterio/safecli-kopia/internal" + "github.com/kanisterio/safecli-kopia/internal/opts" +) + +func TestNewKopiaCommand(t *testing.T) { check.TestingT(t) } + +type KopiaCommandSuite struct{} + +var _ = check.Suite(&KopiaCommandSuite{}) + +func (s *KopiaCommandSuite) TestNewKopiaCommandSuite(c *check.C) { + cmd, err := internal.NewKopiaCommand(opts.JSON(true)) + c.Check(err, check.IsNil) + c.Check(cmd.Build(), check.DeepEquals, []string{"kopia", "--json"}) +} diff --git a/internal/opts/cache_opts.go b/internal/opts/cache_opts.go new file mode 100644 index 0000000..5027ce4 --- /dev/null +++ b/internal/opts/cache_opts.go @@ -0,0 +1,69 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package opts + +import ( + "strconv" + + "github.com/kanisterio/safecli/command" + + "github.com/kanisterio/safecli-kopia/args" +) + +const ( + defaultCacheDirectory = "/tmp/kopia-cache" +) + +// CacheDirectory creates a new cache directory option with a given directory. +// If the directory is empty, the default cache directory is used. +func CacheDirectory(dir string) command.Applier { + if dir == "" { + dir = defaultCacheDirectory + } + return command.NewOptionWithArgument("--cache-directory", dir) +} + +// ContentCacheSizeLimitMB creates a new content cache size option with a given size. +func ContentCacheSizeLimitMB(size int) command.Applier { + val := strconv.Itoa(size) + return command.NewOptionWithArgument("--content-cache-size-limit-mb", val) +} + +// MetadataCacheSizeLimitMB creates a new metadata cache size option with a given size. +func MetadataCacheSizeLimitMB(size int) command.Applier { + val := strconv.Itoa(size) + return command.NewOptionWithArgument("--metadata-cache-size-limit-mb", val) +} + +// ContentCacheSizeMB creates a new content cache size option with a given size. +func ContentCacheSizeMB(size int) command.Applier { + val := strconv.Itoa(size) + return command.NewOptionWithArgument("--content-cache-size-mb", val) +} + +// MetadataCacheSizeMB creates a new metadata cache size option with a given size. +func MetadataCacheSizeMB(size int) command.Applier { + val := strconv.Itoa(size) + return command.NewOptionWithArgument("--metadata-cache-size-mb", val) +} + +// Cache maps the Cache arguments to the CLI command options. +func Cache(args args.Cache) command.Applier { + return command.NewArguments( + CacheDirectory(args.CacheDirectory), + ContentCacheSizeLimitMB(args.ContentCacheSizeLimitMB), + MetadataCacheSizeLimitMB(args.MetadataCacheSizeLimitMB), + ) +} diff --git a/internal/opts/cache_opts_test.go b/internal/opts/cache_opts_test.go new file mode 100644 index 0000000..31e1f80 --- /dev/null +++ b/internal/opts/cache_opts_test.go @@ -0,0 +1,55 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package opts_test + +import ( + "testing" + + "github.com/kanisterio/safecli/command" + "github.com/kanisterio/safecli/test" + "gopkg.in/check.v1" + + "github.com/kanisterio/safecli-kopia/args" + "github.com/kanisterio/safecli-kopia/internal/opts" +) + +func TestCacheOptions(t *testing.T) { check.TestingT(t) } + +var _ = check.Suite(&test.ArgumentSuite{Cmd: "cmd", Arguments: []test.ArgumentTest{ + { + Name: "CacheDirectory", + Argument: command.NewArguments(opts.CacheDirectory(""), opts.CacheDirectory("/path/to/cache")), + ExpectedCLI: []string{"cmd", "--cache-directory=/tmp/kopia-cache", "--cache-directory=/path/to/cache"}, + }, + { + Name: "ContentCacheSizeLimitMB", + Argument: command.NewArguments(opts.ContentCacheSizeLimitMB(0), opts.ContentCacheSizeLimitMB(1024)), + ExpectedCLI: []string{"cmd", "--content-cache-size-limit-mb=0", "--content-cache-size-limit-mb=1024"}, + }, + { + Name: "MetadataCacheSizeLimitMB", + Argument: command.NewArguments(opts.MetadataCacheSizeLimitMB(0), opts.MetadataCacheSizeLimitMB(1024)), + ExpectedCLI: []string{"cmd", "--metadata-cache-size-limit-mb=0", "--metadata-cache-size-limit-mb=1024"}, + }, + { + Name: "Cache", + Argument: opts.Cache(args.Cache{ + CacheDirectory: "/path/to/cache", + ContentCacheSizeLimitMB: 1024, + MetadataCacheSizeLimitMB: 2048, + }), + ExpectedCLI: []string{"cmd", "--cache-directory=/path/to/cache", "--content-cache-size-limit-mb=1024", "--metadata-cache-size-limit-mb=2048"}, + }, +}}) diff --git a/internal/opts/common_opts.go b/internal/opts/common_opts.go new file mode 100644 index 0000000..738d56c --- /dev/null +++ b/internal/opts/common_opts.go @@ -0,0 +1,71 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package opts + +import ( + "github.com/kanisterio/safecli/command" + + "github.com/kanisterio/safecli-kopia/args" +) + +const ( + defaultLogLevel = "error" +) + +// LogDirectory creates a new log directory option with a given directory. +// if the directory is empty, the log directory option is not set. +func LogDirectory(dir string) command.Applier { + if dir == "" { + return command.NewNoopArgument() + } + return command.NewOptionWithArgument("--log-dir", dir) +} + +// LogLevel creates a new log level flag with a given level. +// If the level is empty, the default log level is used. +func LogLevel(level string) command.Applier { + if level == "" { + level = defaultLogLevel + } + return command.NewOptionWithArgument("--log-level", level) +} + +// ConfigFilePath creates a new config file path option with a given path. +// If the path is empty, the config file path option is not set. +func ConfigFilePath(path string) command.Applier { + if path == "" { + return command.NewNoopArgument() + } + return command.NewOptionWithArgument("--config-file", path) +} + +// RepoPassword creates a new repository password option with a given password. +// If the password is empty, the repository password option is not set. +func RepoPassword(password string) command.Applier { + if password == "" { + return command.NewNoopArgument() + } + return command.NewOptionWithRedactedArgument("--password", password) +} + +// Common maps the common arguments to the CLI command options. +func Common(args args.Common) command.Applier { + return command.NewArguments( + ConfigFilePath(args.ConfigFilePath), + LogDirectory(args.LogDirectory), + LogLevel(args.LogLevel), + RepoPassword(args.RepoPassword), + ) +} diff --git a/internal/opts/common_opts_test.go b/internal/opts/common_opts_test.go new file mode 100644 index 0000000..0ee96f6 --- /dev/null +++ b/internal/opts/common_opts_test.go @@ -0,0 +1,61 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package opts_test + +import ( + "testing" + + "github.com/kanisterio/safecli/command" + "github.com/kanisterio/safecli/test" + "gopkg.in/check.v1" + + "github.com/kanisterio/safecli-kopia/args" + "github.com/kanisterio/safecli-kopia/internal/opts" +) + +func TestCommonOptions(t *testing.T) { check.TestingT(t) } + +var _ = check.Suite(&test.ArgumentSuite{Cmd: "cmd", Arguments: []test.ArgumentTest{ + { + Name: "LogDirectory", + Argument: command.NewArguments(opts.LogDirectory(""), opts.LogDirectory("/path/to/logs")), + ExpectedCLI: []string{"cmd", "--log-dir=/path/to/logs"}, + }, + { + Name: "LogLevel", + Argument: command.NewArguments(opts.LogLevel(""), opts.LogLevel("info")), + ExpectedCLI: []string{"cmd", "--log-level=error", "--log-level=info"}, + }, + { + Name: "ConfigFilePath", + Argument: command.NewArguments(opts.ConfigFilePath(""), opts.ConfigFilePath("/path/to/config")), + ExpectedCLI: []string{"cmd", "--config-file=/path/to/config"}, + }, + { + Name: "RepoPassword", + Argument: command.NewArguments(opts.RepoPassword(""), opts.RepoPassword("pass123")), + ExpectedCLI: []string{"cmd", "--password=pass123"}, + }, + { + Name: "Common", + Argument: opts.Common(args.Common{ + LogDirectory: "/path/to/logs", + LogLevel: "trace", + ConfigFilePath: "/path/to/config", + RepoPassword: "pass123", + }), + ExpectedCLI: []string{"cmd", "--config-file=/path/to/config", "--log-dir=/path/to/logs", "--log-level=trace", "--password=pass123"}, + }, +}}) diff --git a/internal/opts/opts.go b/internal/opts/opts.go new file mode 100644 index 0000000..f5d07fc --- /dev/null +++ b/internal/opts/opts.go @@ -0,0 +1,52 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package opts + +import "github.com/kanisterio/safecli/command" + +// All creates a new all option. +func All(enabled bool) command.Applier { + return command.NewOption("--all", enabled) +} + +// Delta creates a new delta option. +func Delta(enabled bool) command.Applier { + return command.NewOption("--delta", enabled) +} + +// ShowIdentical creates a new show identical option. +func ShowIdentical(enabled bool) command.Applier { + return command.NewOption("--show-identical", enabled) +} + +// ReadOnly creates a new read only option. +func ReadOnly(enabled bool) command.Applier { + return command.NewOption("--readonly", enabled) +} + +// CheckForUpdates creates a new check for updates option. +func CheckForUpdates(enabled bool) command.Applier { + return command.NewToggleOption("--check-for-updates", "--no-check-for-updates", enabled) +} + +// JSON creates a new JSON option. +func JSON(enabled bool) command.Applier { + return command.NewOption("--json", enabled) +} + +// Delete creates a new delete option. +func Delete(enabled bool) command.Applier { + return command.NewOption("--delete", enabled) +} diff --git a/internal/opts/opts_test.go b/internal/opts/opts_test.go new file mode 100644 index 0000000..c5c3654 --- /dev/null +++ b/internal/opts/opts_test.go @@ -0,0 +1,65 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package opts_test + +import ( + "testing" + + "github.com/kanisterio/safecli/command" + "github.com/kanisterio/safecli/test" + "gopkg.in/check.v1" + + "github.com/kanisterio/safecli-kopia/internal/opts" +) + +func TestOptions(t *testing.T) { check.TestingT(t) } + +var _ = check.Suite(&test.ArgumentSuite{Cmd: "cmd", Arguments: []test.ArgumentTest{ + { + Name: "All", + Argument: command.NewArguments(opts.All(true), opts.All(false)), + ExpectedCLI: []string{"cmd", "--all"}, + }, + { + Name: "Delta", + Argument: command.NewArguments(opts.Delta(true), opts.Delta(false)), + ExpectedCLI: []string{"cmd", "--delta"}, + }, + { + Name: "ShowIdentical", + Argument: command.NewArguments(opts.ShowIdentical(true), opts.ShowIdentical(false)), + ExpectedCLI: []string{"cmd", "--show-identical"}, + }, + { + Name: "Readonly", + Argument: command.NewArguments(opts.ReadOnly(true), opts.ReadOnly(false)), + ExpectedCLI: []string{"cmd", "--readonly"}, + }, + { + Name: "CheckForUpdates", + Argument: command.NewArguments(opts.CheckForUpdates(true), opts.CheckForUpdates(false)), + ExpectedCLI: []string{"cmd", "--check-for-updates", "--no-check-for-updates"}, + }, + { + Name: "JSON", + Argument: command.NewArguments(opts.JSON(true), opts.JSON(false)), + ExpectedCLI: []string{"cmd", "--json"}, + }, + { + Name: "Delete", + Argument: command.NewArguments(opts.Delete(true), opts.Delete(false)), + ExpectedCLI: []string{"cmd", "--delete"}, + }, +}}) diff --git a/internal/path.go b/internal/path.go new file mode 100644 index 0000000..9085ebc --- /dev/null +++ b/internal/path.go @@ -0,0 +1,28 @@ +// Copyright 2022 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "path" +) + +// GenerateFullRepoPath generates the full repository path. +// If the location-specific prefix is empty, the repository-specific prefix is returned. +func GenerateFullRepoPath(locPrefix, repoPathPrefix string) string { + if locPrefix != "" { + return path.Join(locPrefix, repoPathPrefix) + "/" + } + return repoPathPrefix +} diff --git a/internal/path_test.go b/internal/path_test.go new file mode 100644 index 0000000..c34edea --- /dev/null +++ b/internal/path_test.go @@ -0,0 +1,52 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal_test + +import ( + "testing" + + "gopkg.in/check.v1" + + "github.com/kanisterio/safecli-kopia/internal" +) + +func TestPath(t *testing.T) { check.TestingT(t) } + +type PathSuite struct{} + +var _ = check.Suite(&PathSuite{}) + +func (s *PathSuite) TestGenerateFullRepoPath(c *check.C) { + tests := []struct { + locPrefix string + repoPathPrefix string + expected string + }{ + { + locPrefix: "", + repoPathPrefix: "repo", + expected: "repo", + }, + { + locPrefix: "loc", + repoPathPrefix: "repo", + expected: "loc/repo/", + }, + } + for _, test := range tests { + got := internal.GenerateFullRepoPath(test.locPrefix, test.repoPathPrefix) + c.Check(got, check.Equals, test.expected) + } +} diff --git a/internal/test/command_suite.go b/internal/test/command_suite.go new file mode 100644 index 0000000..57a8c2f --- /dev/null +++ b/internal/test/command_suite.go @@ -0,0 +1,113 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "github.com/kanisterio/safecli" + "github.com/kanisterio/safecli/test" + "github.com/pkg/errors" + "gopkg.in/check.v1" +) + +// CommandTest defines a single test for a command. +type CommandTest struct { + // Name of the test. (required) + Name string + + // Command to test. (required) + Command func() (*safecli.Builder, error) + + // Expected CLI arguments. (optional) + ExpectedCLI []string + + // Expected log output. (optional) + // if empty, it will be set to ExpectedCLI joined with space. + // if empty and ExpectedCLI is empty, it will be ignored. + ExpectedLog string + + // Expected error. (optional) + // If nil, no error is expected and + // ExpectedCLI and ExpectedLog are checked. + ExpectedErr error +} + +// CheckCommentString implements check.CommentInterface +func (t *CommandTest) CheckCommentString() string { + return t.Name +} + +// setDefaultExpectedLog sets the default value for ExpectedLog based on ExpectedCLI. +func (t *CommandTest) setDefaultExpectedLog() { + if len(t.ExpectedLog) == 0 && len(t.ExpectedCLI) > 0 { + t.ExpectedLog = test.RedactCLI(t.ExpectedCLI) + } +} + +// assertNoError makes sure there is no error. +func (t *CommandTest) assertNoError(c *check.C, err error) { + c.Assert(err, check.IsNil) +} + +// assertError checks the error against ExpectedErr. +func (t *CommandTest) assertError(c *check.C, err error) { + actualErr := errors.Cause(err) + c.Assert(actualErr, check.Equals, t.ExpectedErr) +} + +// assertCLI asserts the builder's CLI output against ExpectedCLI. +func (t *CommandTest) assertCLI(c *check.C, b *safecli.Builder) { + if t.ExpectedCLI != nil { + c.Check(b.Build(), check.DeepEquals, t.ExpectedCLI) + } +} + +// assertLog asserts the builder's log output against ExpectedLog. +func (t *CommandTest) assertLog(c *check.C, b *safecli.Builder) { + if t.ExpectedCLI != nil { + t.setDefaultExpectedLog() + c.Check(b.String(), check.Equals, t.ExpectedLog) + } +} + +func (t *CommandTest) Test(c *check.C) { + cmd, err := t.Command() + if t.Name != "" { + c.Log(t.Name) + } + if t.ExpectedErr == nil { + t.assertNoError(c, err) + } else { + t.assertError(c, err) + } + t.assertCLI(c, cmd) + t.assertLog(c, cmd) +} + +// CommandSuite defines a test suite for commands. +type CommandSuite struct { + Commands []CommandTest +} + +// TestCommands runs all tests in the suite. +func (s *CommandSuite) TestCommands(c *check.C) { + for _, cmd := range s.Commands { + cmd.Test(c) + } +} + +// NewCommandSuite creates a new CommandSuite. +func NewCommandSuite(commands []CommandTest) *CommandSuite { + return &CommandSuite{Commands: commands} +} diff --git a/repository/data_test.go b/repository/data_test.go new file mode 100644 index 0000000..0fe667f --- /dev/null +++ b/repository/data_test.go @@ -0,0 +1,80 @@ +// Copyright 2024 The Kanister Autho +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repository + +import ( + "time" + + "github.com/kanisterio/safecli-kopia/args" + "github.com/kanisterio/safecli-kopia/repository/storage/location" +) + +var ( + common = args.Common{ + RepoPassword: "encr-key", + ConfigFilePath: "path/kopia.config", + LogDirectory: "cache/log", + } + + cache = args.Cache{ + CacheDirectory: "/tmp/cache.dir", + ContentCacheSizeLimitMB: 0, + MetadataCacheSizeLimitMB: 0, + } +) + +var ( + retentionMode = "Locked" + retentionPeriod = 15 * time.Minute + + locFS = location.Location{ + Type: "filestore", + Prefix: "test-prefix", + } + + locAzure = location.Location{ + Type: "azure", + BucketName: "test-bucket", + Prefix: "test-prefix", + } + + locGCS = location.Location{ + Type: "gcs", + BucketName: "test-bucket", + Prefix: "test-prefix", + CredentialsFile: "/tmp/creds.txt", + } + + locS3 = location.Location{ + Type: "s3", + Endpoint: "test-endpoint", + Region: "test-region", + BucketName: "test-bucket", + Prefix: "test-prefix", + HasSkipSSLVerify: false, + } + + locS3Compliant = location.Location{ + Type: "s3Compliant", + Endpoint: "test-endpoint", + Region: "test-region", + BucketName: "test-bucket", + Prefix: "test-prefix", + HasSkipSSLVerify: false, + } + + locFTP = location.Location{ + Type: "ftp", + } +) diff --git a/repository/opts.go b/repository/opts.go new file mode 100644 index 0000000..5d088da --- /dev/null +++ b/repository/opts.go @@ -0,0 +1,123 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repository + +import ( + "time" + + "github.com/go-openapi/strfmt" + "github.com/kanisterio/safecli/command" + "github.com/pkg/errors" + + cli "github.com/kanisterio/safecli-kopia" + "github.com/kanisterio/safecli-kopia/repository/storage/azure" + "github.com/kanisterio/safecli-kopia/repository/storage/fs" + "github.com/kanisterio/safecli-kopia/repository/storage/gcs" + "github.com/kanisterio/safecli-kopia/repository/storage/location" + "github.com/kanisterio/safecli-kopia/repository/storage/s3" +) + +var ( + cmdRepository = command.NewArgument("repository") + + subcmdCreate = command.NewArgument("create") + subcmdConnect = command.NewArgument("connect") + subcmdServer = command.NewArgument("server") + subcmdSetParameters = command.NewArgument("set-parameters") + subcmdStatus = command.NewArgument("status") +) + +// optHostname creates a new option for the hostname of the repository. +// If the hostname is empty, the hostname option is not set. +func optHostname(h string) command.Applier { + if h == "" { + return command.NewNoopArgument() + } + return command.NewOptionWithArgument("--override-hostname", h) +} + +// optUsername creates a new option for the username of the repository. +// If the username is empty, the username option is not set. +func optUsername(u string) command.Applier { + if u == "" { + return command.NewNoopArgument() + } + return command.NewOptionWithArgument("--override-username", u) +} + +// optBlobRetention creates new blob retention options with a given mode and period. +// If mode is empty, the retention is disabled. +func optBlobRetention(mode string, period time.Duration) command.Applier { + if mode == "" { + return command.NewNoopArgument() + } + return command.NewArguments( + command.NewOptionWithArgument("--retention-mode", mode), + command.NewOptionWithArgument("--retention-period", period.String()), + ) +} + +type storageBuilder func(location.Location, string) command.Applier + +var storageBuilders = map[location.LocType]storageBuilder{ + location.LocTypeFilestore: fs.New, + location.LocTypeAzure: azure.New, + location.LocTypeS3: s3.New, + location.LocTypes3Compliant: s3.New, + location.LocTypeGCS: gcs.New, +} + +// optStorage creates a list of options for the specified storage location. +func optStorage(l location.Location, repoPathPrefix string) command.Applier { + sb := storageBuilders[l.Type] + if sb == nil { + return errUnsupportedStorageType(l.Type) + } + return sb(l, repoPathPrefix) +} + +func errUnsupportedStorageType(t location.LocType) command.Applier { + err := errors.Wrapf(cli.ErrUnsupportedStorage, "storage location: %v", t) + return command.NewErrorArgument(err) +} + +// optReadOnly creates a new option for the read-only mode of the repository. +func optReadOnly(readOnly bool) command.Applier { + return command.NewOption("--readonly", readOnly) +} + +// optPointInTime creates a new option for the point-in-time of the repository. +func optPointInTime(l location.Location, pit strfmt.DateTime) command.Applier { + if !l.IsPointInTypeSupported() || time.Time(pit).IsZero() { + return command.NewNoopArgument() + } + return command.NewOptionWithArgument("--point-in-time", pit.String()) +} + +// optServerURL creates a new server URL flag with a given server URL. +func optServerURL(serverURL string) command.Applier { + if serverURL == "" { + return command.NewErrorArgument(cli.ErrInvalidServerURL) + } + return command.NewOptionWithArgument("--url", serverURL) +} + +// optServerCertFingerprint creates a new server certificate fingerprint flag with a given fingerprint. +func optServerCertFingerprint(fingerprint string) command.Applier { + if fingerprint == "" { + return command.NewNoopArgument() + } + return command.NewOptionWithRedactedArgument("--server-cert-fingerprint", fingerprint) +} diff --git a/repository/opts_test.go b/repository/opts_test.go new file mode 100644 index 0000000..0d8e794 --- /dev/null +++ b/repository/opts_test.go @@ -0,0 +1,133 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repository + +import ( + "fmt" + "testing" + + "github.com/go-openapi/strfmt" + "github.com/kanisterio/safecli/command" + "github.com/kanisterio/safecli/test" + "gopkg.in/check.v1" + + cli "github.com/kanisterio/safecli-kopia" + "github.com/kanisterio/safecli-kopia/repository/storage/location" +) + +func TestRepositoryOptions(t *testing.T) { check.TestingT(t) } + +var _ = check.Suite(&test.ArgumentSuite{Cmd: "cmd", Arguments: []test.ArgumentTest{ + { + Name: "optHostname", + Argument: command.NewArguments( + optHostname("host"), + optHostname(""), // no output + ), + ExpectedCLI: []string{"cmd", "--override-hostname=host"}, + }, + { + Name: "optUsername", + Argument: command.NewArguments( + optUsername("user"), + optUsername(""), // no output + ), + ExpectedCLI: []string{"cmd", "--override-username=user"}, + }, + { + Name: "optBlobRetention", + Argument: command.NewArguments( + optBlobRetention(retentionMode, retentionPeriod), + optBlobRetention("", 0), // no output + ), + ExpectedCLI: []string{"cmd", "--retention-mode=Locked", "--retention-period=15m0s"}, + }, + { + Name: "optStorage FS", + Argument: optStorage(locFS, "repoPathPrefix"), + ExpectedCLI: []string{"cmd", "filesystem", "--path=/mnt/data/test-prefix/repoPathPrefix/"}, + }, + { + Name: "optStorage Azure", + Argument: optStorage(locAzure, "repoPathPrefix"), + ExpectedCLI: []string{"cmd", "azure", "--container=test-bucket", "--prefix=test-prefix/repoPathPrefix/"}, + }, + { + Name: "optStorage S3", + Argument: optStorage(locS3, "repoPathPrefix"), + ExpectedCLI: []string{"cmd", "s3", "--region=test-region", "--bucket=test-bucket", "--endpoint=test-endpoint", "--prefix=test-prefix/repoPathPrefix/"}, + }, + { + Name: "optStorage S3Compliant", + Argument: optStorage(locS3Compliant, "repoPathPrefix"), + ExpectedCLI: []string{"cmd", "s3", "--region=test-region", "--bucket=test-bucket", "--endpoint=test-endpoint", "--prefix=test-prefix/repoPathPrefix/"}, + }, + { + Name: "optStorage FTP Unsupported", + Argument: optStorage(locFTP, "repoPathPrefix"), + ExpectedErr: cli.ErrUnsupportedStorage, + }, + { + Name: "optReadOnly", + Argument: command.NewArguments( + optReadOnly(true), + optReadOnly(false), // no output + ), + ExpectedCLI: []string{"cmd", "--readonly"}, + }, + { + Name: "optPointInTime supported only for azure and s3", + Argument: func() command.Arguments { + locations := []location.Location{ + locFS, // no output + locAzure, // idx: 1 + locGCS, // no output + locS3, // idx: 3 + locS3Compliant, // idx: 4 + } + var args command.Arguments + for idx, l := range locations { + t, _ := strfmt.ParseDateTime( + fmt.Sprintf("2021-02-%02dT01:02:03.000Z", idx), + ) + args = append(args, optPointInTime(l, t)) + } + return args + }(), + ExpectedCLI: []string{"cmd", + "--point-in-time=2021-02-01T01:02:03.000Z", + "--point-in-time=2021-02-03T01:02:03.000Z", + "--point-in-time=2021-02-04T01:02:03.000Z", + }, + }, + { + Name: "optServerURL with ServerURL should return option", + Argument: optServerURL("http://test-server"), + ExpectedCLI: []string{"cmd", "--url=http://test-server"}, + }, + { + Name: "optServerURL with empty ServerURL should return error", + Argument: optServerURL(""), + ExpectedErr: cli.ErrInvalidServerURL, + }, + { + Name: "optServerCertFingerprint", + Argument: command.NewArguments( + optServerCertFingerprint("fingerprint"), + optServerCertFingerprint(""), // no output + ), + ExpectedCLI: []string{"cmd", "--server-cert-fingerprint=fingerprint"}, + }, +}}) diff --git a/repository/repository_connect.go b/repository/repository_connect.go new file mode 100644 index 0000000..b105742 --- /dev/null +++ b/repository/repository_connect.go @@ -0,0 +1,57 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repository + +import ( + "github.com/go-openapi/strfmt" + + "github.com/kanisterio/safecli" + + "github.com/kanisterio/safecli-kopia/args" + "github.com/kanisterio/safecli-kopia/internal" + "github.com/kanisterio/safecli-kopia/internal/opts" + "github.com/kanisterio/safecli-kopia/repository/storage/location" +) + +// ConnectArgs defines the arguments for the `kopia repository connect` command. +type ConnectArgs struct { + args.Common + args.Cache + + Hostname string // the hostname of the repository + Username string // the username of the repository + Location location.Location // the location of the repository + RepoPathPrefix string // the prefix of the repository path + ReadOnly bool // connect to a repository in read-only mode + PointInTime strfmt.DateTime // connect to a repository as it was at a specific point in time +} + +// Connect creates a new `kopia repository connect ...` command. +func Connect(args ConnectArgs) (*safecli.Builder, error) { + return internal.NewKopiaCommand( + opts.Common(args.Common), + cmdRepository, subcmdConnect, + opts.CheckForUpdates(false), + optReadOnly(args.ReadOnly), + opts.Cache(args.Cache), + optHostname(args.Hostname), + optUsername(args.Username), + optStorage( + args.Location, + args.RepoPathPrefix, + ), + optPointInTime(args.Location, args.PointInTime), + ) +} diff --git a/repository/repository_connect_server.go b/repository/repository_connect_server.go new file mode 100644 index 0000000..613e569 --- /dev/null +++ b/repository/repository_connect_server.go @@ -0,0 +1,55 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repository + +import ( + "github.com/kanisterio/safecli" + "github.com/kanisterio/safecli/command" + + "github.com/kanisterio/safecli-kopia/args" + "github.com/kanisterio/safecli-kopia/internal" + "github.com/kanisterio/safecli-kopia/internal/opts" +) + +// ConnectServerArgs defines the arguments for the `kopia repository connect server` command. +type ConnectServerArgs struct { + args.Common + args.Cache + + Hostname string // hostname of the repository + Username string // username of the repository + ServerURL string // URL of the Kopia Repository API server + Fingerprint string // fingerprint of the server's TLS certificate + ReadOnly bool // connect to a repository in read-only mode +} + +// ConnectServer creates a new `kopia repository connect server...` command. +func ConnectServer(connectServerArgs ConnectServerArgs) (*safecli.Builder, error) { + appliers := []command.Applier{ + opts.Common(connectServerArgs.Common), + cmdRepository, subcmdConnect, subcmdServer, + opts.CheckForUpdates(false), + optReadOnly(connectServerArgs.ReadOnly), + opts.Cache(connectServerArgs.Cache), + optHostname(connectServerArgs.Hostname), + optUsername(connectServerArgs.Username), + optServerURL(connectServerArgs.ServerURL), + optServerCertFingerprint(connectServerArgs.Fingerprint), + } + appliers = append(appliers, args.RepositoryConnectServer.CommandAppliers()...) + return internal.NewKopiaCommand( + appliers..., + ) +} diff --git a/repository/repository_connect_server_test.go b/repository/repository_connect_server_test.go new file mode 100644 index 0000000..a8187ec --- /dev/null +++ b/repository/repository_connect_server_test.go @@ -0,0 +1,104 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repository + +import ( + "testing" + + "github.com/kanisterio/safecli" + "gopkg.in/check.v1" + + "github.com/kanisterio/safecli-kopia/args" + "github.com/kanisterio/safecli-kopia/internal/test" +) + +func TestRepositoryConnectServerCommand(t *testing.T) { check.TestingT(t) } + +// Test Repository Connect Server command +var _ = check.Suite(test.NewCommandSuite([]test.CommandTest{ + { + Name: "repository connect server", + Command: func() (*safecli.Builder, error) { + args := ConnectServerArgs{ + Common: common, + Cache: cache, + Hostname: "test-hostname", + Username: "test-username", + ServerURL: "http://test-server", + Fingerprint: "test-fingerprint", + ReadOnly: true, + } + return ConnectServer(args) + }, + ExpectedCLI: []string{"kopia", + "--config-file=path/kopia.config", + "--log-dir=cache/log", + "--log-level=error", + "--password=encr-key", + "repository", + "connect", + "server", + "--no-check-for-updates", + "--readonly", + "--cache-directory=/tmp/cache.dir", + "--content-cache-size-limit-mb=0", + "--metadata-cache-size-limit-mb=0", + "--override-hostname=test-hostname", + "--override-username=test-username", + "--url=http://test-server", + "--server-cert-fingerprint=test-fingerprint", + }, + }, + { + Name: "repository connect server, with additonal args", + Command: func() (*safecli.Builder, error) { + arguments := ConnectServerArgs{ + Common: common, + Cache: cache, + Hostname: "test-hostname", + Username: "test-username", + ServerURL: "http://test-server", + Fingerprint: "test-fingerprint", + ReadOnly: true, + } + + flags := args.RepositoryConnectServer + args.RepositoryConnectServer = args.Args{} + args.RepositoryConnectServer.Set("--testflag", "testvalue") + defer func() { args.RepositoryConnectServer = flags }() + + return ConnectServer(arguments) + }, + ExpectedCLI: []string{"kopia", + "--config-file=path/kopia.config", + "--log-dir=cache/log", + "--log-level=error", + "--password=encr-key", + "repository", + "connect", + "server", + "--no-check-for-updates", + "--readonly", + "--cache-directory=/tmp/cache.dir", + "--content-cache-size-limit-mb=0", + "--metadata-cache-size-limit-mb=0", + "--override-hostname=test-hostname", + "--override-username=test-username", + "--url=http://test-server", + "--server-cert-fingerprint=test-fingerprint", + "--testflag=testvalue", + }, + }, +})) diff --git a/repository/repository_connect_test.go b/repository/repository_connect_test.go new file mode 100644 index 0000000..87ecc49 --- /dev/null +++ b/repository/repository_connect_test.go @@ -0,0 +1,131 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repository + +import ( + "testing" + + "github.com/go-openapi/strfmt" + "github.com/kanisterio/safecli" + "gopkg.in/check.v1" + + "github.com/kanisterio/safecli-kopia/internal/test" +) + +func TestRepositoryConnectCommand(t *testing.T) { check.TestingT(t) } + +// Test Repository Connect command +var _ = check.Suite(test.NewCommandSuite([]test.CommandTest{ + { + Name: "repository connect with default retention", + Command: func() (*safecli.Builder, error) { + args := ConnectArgs{ + Common: common, + Cache: cache, + Hostname: "test-hostname", + Username: "test-username", + RepoPathPrefix: "test-path/prefix", + Location: locFS, + } + return Connect(args) + }, + ExpectedCLI: []string{"kopia", + "--config-file=path/kopia.config", + "--log-dir=cache/log", + "--log-level=error", + "--password=encr-key", + "repository", + "connect", + "--no-check-for-updates", + "--cache-directory=/tmp/cache.dir", + "--content-cache-size-limit-mb=0", + "--metadata-cache-size-limit-mb=0", + "--override-hostname=test-hostname", + "--override-username=test-username", + "filesystem", + "--path=/mnt/data/test-prefix/test-path/prefix/", + }, + }, + { + Name: "repository connect with ReadOnly", + Command: func() (*safecli.Builder, error) { + args := ConnectArgs{ + Common: common, + Cache: cache, + Hostname: "test-hostname", + Username: "test-username", + RepoPathPrefix: "test-path/prefix", + Location: locFS, + ReadOnly: true, + } + return Connect(args) + }, + ExpectedCLI: []string{"kopia", + "--config-file=path/kopia.config", + "--log-dir=cache/log", + "--log-level=error", + "--password=encr-key", + "repository", + "connect", + "--no-check-for-updates", + "--readonly", + "--cache-directory=/tmp/cache.dir", + "--content-cache-size-limit-mb=0", + "--metadata-cache-size-limit-mb=0", + "--override-hostname=test-hostname", + "--override-username=test-username", + "filesystem", + "--path=/mnt/data/test-prefix/test-path/prefix/", + }, + }, + { + Name: "repository connect with PIT and ReadOnly", + Command: func() (*safecli.Builder, error) { + pit, _ := strfmt.ParseDateTime("2021-02-03T01:02:03Z") + args := ConnectArgs{ + Common: common, + Cache: cache, + Hostname: "test-hostname", + Username: "test-username", + RepoPathPrefix: "path/prefix", + Location: locS3, + PointInTime: pit, + ReadOnly: true, + } + return Connect(args) + }, + ExpectedCLI: []string{"kopia", + "--config-file=path/kopia.config", + "--log-dir=cache/log", + "--log-level=error", + "--password=encr-key", + "repository", + "connect", + "--no-check-for-updates", + "--readonly", + "--cache-directory=/tmp/cache.dir", + "--content-cache-size-limit-mb=0", + "--metadata-cache-size-limit-mb=0", + "--override-hostname=test-hostname", + "--override-username=test-username", + "s3", + "--region=test-region", + "--bucket=test-bucket", + "--endpoint=test-endpoint", + "--prefix=test-prefix/path/prefix/", + "--point-in-time=2021-02-03T01:02:03.000Z", + }, + }, +})) diff --git a/repository/repository_create.go b/repository/repository_create.go new file mode 100644 index 0000000..a80b58e --- /dev/null +++ b/repository/repository_create.go @@ -0,0 +1,61 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repository + +import ( + "time" + + "github.com/kanisterio/safecli" + "github.com/kanisterio/safecli/command" + + "github.com/kanisterio/safecli-kopia/args" + "github.com/kanisterio/safecli-kopia/internal" + "github.com/kanisterio/safecli-kopia/internal/opts" + "github.com/kanisterio/safecli-kopia/repository/storage/location" +) + +// CreateArgs defines the arguments for the `kopia repository create` command. +type CreateArgs struct { + args.Common // embeds common arguments + args.Cache // embeds cache arguments + + Hostname string // the hostname of the repository + Username string // the username of the repository + Location location.Location // the location of the repository + RepoPathPrefix string // the prefix of the repository path + RetentionMode string // retention mode for supported storage backends + RetentionPeriod time.Duration // retention period for supported storage backends +} + +// Create creates a new `kopia repository create ...` command. +func Create(createArgs CreateArgs) (*safecli.Builder, error) { + appliers := []command.Applier{ + opts.Common(createArgs.Common), + cmdRepository, subcmdCreate, + opts.CheckForUpdates(false), + opts.Cache(createArgs.Cache), + optHostname(createArgs.Hostname), + optUsername(createArgs.Username), + optBlobRetention(createArgs.RetentionMode, createArgs.RetentionPeriod), + optStorage( + createArgs.Location, + createArgs.RepoPathPrefix, + ), + } + appliers = append(appliers, args.RepositoryCreate.CommandAppliers()...) + return internal.NewKopiaCommand( + appliers..., + ) +} diff --git a/repository/repository_create_test.go b/repository/repository_create_test.go new file mode 100644 index 0000000..a1ed945 --- /dev/null +++ b/repository/repository_create_test.go @@ -0,0 +1,265 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repository + +import ( + "testing" + + "github.com/kanisterio/safecli" + "gopkg.in/check.v1" + + "github.com/kanisterio/safecli-kopia" + "github.com/kanisterio/safecli-kopia/args" + "github.com/kanisterio/safecli-kopia/internal/test" +) + +func TestRepositoryCreateCommand(t *testing.T) { check.TestingT(t) } + +// Test Repository Create command +var _ = check.Suite(test.NewCommandSuite([]test.CommandTest{ + { + Name: "repository create with no storage", + Command: func() (*safecli.Builder, error) { + args := CreateArgs{ + Common: common, + Cache: cache, + Hostname: "test-hostname", + Username: "test-username", + RepoPathPrefix: "test-path/prefix", + } + return Create(args) + }, + ExpectedErr: cli.ErrUnsupportedStorage, + }, + { + Name: "repository create with filestore location", + Command: func() (*safecli.Builder, error) { + args := CreateArgs{ + Common: common, + Cache: cache, + Hostname: "test-hostname", + Username: "test-username", + RepoPathPrefix: "test-path/prefix", + Location: locFS, + RetentionMode: retentionMode, + RetentionPeriod: retentionPeriod, + } + return Create(args) + }, + ExpectedCLI: []string{"kopia", + "--config-file=path/kopia.config", + "--log-dir=cache/log", + "--log-level=error", + "--password=encr-key", + "repository", + "create", + "--no-check-for-updates", + "--cache-directory=/tmp/cache.dir", + "--content-cache-size-limit-mb=0", + "--metadata-cache-size-limit-mb=0", + "--override-hostname=test-hostname", + "--override-username=test-username", + "--retention-mode=Locked", + "--retention-period=15m0s", + "filesystem", + "--path=/mnt/data/test-prefix/test-path/prefix/", + }, + }, + { + Name: "repository create with azure location", + Command: func() (*safecli.Builder, error) { + args := CreateArgs{ + Common: common, + Cache: cache, + Hostname: "test-hostname", + Username: "test-username", + RepoPathPrefix: "test-path/prefix", + Location: locAzure, + RetentionMode: retentionMode, + RetentionPeriod: retentionPeriod, + } + return Create(args) + }, + ExpectedCLI: []string{"kopia", + "--config-file=path/kopia.config", + "--log-dir=cache/log", + "--log-level=error", + "--password=encr-key", + "repository", + "create", + "--no-check-for-updates", + "--cache-directory=/tmp/cache.dir", + "--content-cache-size-limit-mb=0", + "--metadata-cache-size-limit-mb=0", + "--override-hostname=test-hostname", + "--override-username=test-username", + "--retention-mode=Locked", + "--retention-period=15m0s", + "azure", + "--container=test-bucket", + "--prefix=test-prefix/test-path/prefix/", + }, + }, + { + Name: "repository create with gcs location", + Command: func() (*safecli.Builder, error) { + args := CreateArgs{ + Common: common, + Cache: cache, + Hostname: "test-hostname", + Username: "test-username", + RepoPathPrefix: "test-path/prefix", + Location: locGCS, + RetentionMode: retentionMode, + RetentionPeriod: retentionPeriod, + } + return Create(args) + }, + ExpectedCLI: []string{"kopia", + "--config-file=path/kopia.config", + "--log-dir=cache/log", + "--log-level=error", + "--password=encr-key", + "repository", + "create", + "--no-check-for-updates", + "--cache-directory=/tmp/cache.dir", + "--content-cache-size-limit-mb=0", + "--metadata-cache-size-limit-mb=0", + "--override-hostname=test-hostname", + "--override-username=test-username", + "--retention-mode=Locked", + "--retention-period=15m0s", + "gcs", + "--bucket=test-bucket", + "--credentials-file=/tmp/creds.txt", + "--prefix=test-prefix/test-path/prefix/", + }, + }, + { + Name: "repository create with s3 location", + Command: func() (*safecli.Builder, error) { + args := CreateArgs{ + Common: common, + Cache: cache, + Hostname: "test-hostname", + Username: "test-username", + RepoPathPrefix: "test-path/prefix", + Location: locS3, + RetentionMode: retentionMode, + RetentionPeriod: retentionPeriod, + } + return Create(args) + }, + ExpectedCLI: []string{"kopia", + "--config-file=path/kopia.config", + "--log-dir=cache/log", + "--log-level=error", + "--password=encr-key", + "repository", + "create", + "--no-check-for-updates", + "--cache-directory=/tmp/cache.dir", + "--content-cache-size-limit-mb=0", + "--metadata-cache-size-limit-mb=0", + "--override-hostname=test-hostname", + "--override-username=test-username", + "--retention-mode=Locked", + "--retention-period=15m0s", + "s3", + "--region=test-region", + "--bucket=test-bucket", + "--endpoint=test-endpoint", + "--prefix=test-prefix/test-path/prefix/", + }, + }, + { + Name: "repository create with s3 compliant location", + Command: func() (*safecli.Builder, error) { + args := CreateArgs{ + Common: common, + Cache: cache, + Hostname: "test-hostname", + Username: "test-username", + RepoPathPrefix: "test-path/prefix", + Location: locS3Compliant, + RetentionMode: retentionMode, + RetentionPeriod: retentionPeriod, + } + return Create(args) + }, + ExpectedCLI: []string{"kopia", + "--config-file=path/kopia.config", + "--log-dir=cache/log", + "--log-level=error", + "--password=encr-key", + "repository", + "create", + "--no-check-for-updates", + "--cache-directory=/tmp/cache.dir", + "--content-cache-size-limit-mb=0", + "--metadata-cache-size-limit-mb=0", + "--override-hostname=test-hostname", + "--override-username=test-username", + "--retention-mode=Locked", + "--retention-period=15m0s", + "s3", + "--region=test-region", + "--bucket=test-bucket", + "--endpoint=test-endpoint", + "--prefix=test-prefix/test-path/prefix/", + }, + }, + { + Name: "repository create with filestore location and additional args", + Command: func() (*safecli.Builder, error) { + arguments := CreateArgs{ + Common: common, + Cache: cache, + Hostname: "test-hostname", + Username: "test-username", + RepoPathPrefix: "test-path/prefix", + Location: locFS, + RetentionMode: retentionMode, + RetentionPeriod: retentionPeriod, + } + flags := args.RepositoryCreate + args.RepositoryCreate = args.Args{} + args.RepositoryCreate.Set("--testflag", "testvalue") + defer func() { args.RepositoryCreate = flags }() + + return Create(arguments) + }, + ExpectedCLI: []string{"kopia", + "--config-file=path/kopia.config", + "--log-dir=cache/log", + "--log-level=error", + "--password=encr-key", + "repository", + "create", + "--no-check-for-updates", + "--cache-directory=/tmp/cache.dir", + "--content-cache-size-limit-mb=0", + "--metadata-cache-size-limit-mb=0", + "--override-hostname=test-hostname", + "--override-username=test-username", + "--retention-mode=Locked", + "--retention-period=15m0s", + "filesystem", + "--path=/mnt/data/test-prefix/test-path/prefix/", + "--testflag=testvalue", + }, + }, +})) diff --git a/repository/repository_set_parameters.go b/repository/repository_set_parameters.go new file mode 100644 index 0000000..85f9767 --- /dev/null +++ b/repository/repository_set_parameters.go @@ -0,0 +1,42 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repository + +import ( + "time" + + "github.com/kanisterio/safecli" + + "github.com/kanisterio/safecli-kopia/args" + "github.com/kanisterio/safecli-kopia/internal" + "github.com/kanisterio/safecli-kopia/internal/opts" +) + +// SetParametersArgs defines the arguments for the `kopia repository set-parameters ...` command. +type SetParametersArgs struct { + args.Common + + RetentionMode string // retention mode for supported storage backends + RetentionPeriod time.Duration // retention period for supported storage backends +} + +// SetParameters creates a new `kopia repository set-parameters ...` command. +func SetParameters(args SetParametersArgs) (*safecli.Builder, error) { + return internal.NewKopiaCommand( + opts.Common(args.Common), + cmdRepository, subcmdSetParameters, + optBlobRetention(args.RetentionMode, args.RetentionPeriod), + ) +} diff --git a/repository/repository_set_parameters_test.go b/repository/repository_set_parameters_test.go new file mode 100644 index 0000000..817d371 --- /dev/null +++ b/repository/repository_set_parameters_test.go @@ -0,0 +1,88 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repository + +import ( + "testing" + + "github.com/kanisterio/safecli" + "gopkg.in/check.v1" + + "github.com/kanisterio/safecli-kopia/internal/test" +) + +func TestRepositorySetParametersCommand(t *testing.T) { check.TestingT(t) } + +// Test Repository Set Parameters command +var _ = check.Suite(test.NewCommandSuite([]test.CommandTest{ + { + Name: "repository set-parameters with default retention", + Command: func() (*safecli.Builder, error) { + args := SetParametersArgs{ + Common: common, + } + return SetParameters(args) + }, + ExpectedCLI: []string{"kopia", + "--config-file=path/kopia.config", + "--log-dir=cache/log", + "--log-level=error", + "--password=encr-key", + "repository", + "set-parameters", + }, + }, + { + Name: "repository set-parameters with custom retention args", + Command: func() (*safecli.Builder, error) { + args := SetParametersArgs{ + Common: common, + RetentionMode: retentionMode, + RetentionPeriod: retentionPeriod, + } + return SetParameters(args) + }, + ExpectedCLI: []string{"kopia", + "--config-file=path/kopia.config", + "--log-dir=cache/log", + "--log-level=error", + "--password=encr-key", + "repository", + "set-parameters", + "--retention-mode=Locked", + "--retention-period=15m0s", + }, + }, + { + Name: "repository set-parameters with custom retention mode only", + Command: func() (*safecli.Builder, error) { + args := SetParametersArgs{ + Common: common, + RetentionMode: retentionMode, + } + return SetParameters(args) + }, + ExpectedCLI: []string{"kopia", + "--config-file=path/kopia.config", + "--log-dir=cache/log", + "--log-level=error", + "--password=encr-key", + "repository", + "set-parameters", + "--retention-mode=Locked", + "--retention-period=0s", + }, + }, +})) diff --git a/repository/repository_status.go b/repository/repository_status.go new file mode 100644 index 0000000..5fb3fba --- /dev/null +++ b/repository/repository_status.go @@ -0,0 +1,39 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repository + +import ( + "github.com/kanisterio/safecli" + + "github.com/kanisterio/safecli-kopia/args" + "github.com/kanisterio/safecli-kopia/internal" + "github.com/kanisterio/safecli-kopia/internal/opts" +) + +// StatusArgs defines the arguments for the `kopia repository status ...` command. +type StatusArgs struct { + args.Common // embed common arguments + + JSONOutput bool // shows the output in JSON format +} + +// Status creates a new `kopia repository status ...` command. +func Status(args StatusArgs) (*safecli.Builder, error) { + return internal.NewKopiaCommand( + opts.Common(args.Common), + cmdRepository, subcmdStatus, + opts.JSON(args.JSONOutput), + ) +} diff --git a/repository/repository_status_test.go b/repository/repository_status_test.go new file mode 100644 index 0000000..c92e4e6 --- /dev/null +++ b/repository/repository_status_test.go @@ -0,0 +1,66 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package repository + +import ( + "testing" + + "github.com/kanisterio/safecli" + "gopkg.in/check.v1" + + "github.com/kanisterio/safecli-kopia/internal/test" +) + +func TestRepositoryStatusCommand(t *testing.T) { check.TestingT(t) } + +// Test Repository Status command +var _ = check.Suite(test.NewCommandSuite([]test.CommandTest{ + { + Name: "repository status with default args", + Command: func() (*safecli.Builder, error) { + args := StatusArgs{ + Common: common, + } + return Status(args) + }, + ExpectedCLI: []string{"kopia", + "--config-file=path/kopia.config", + "--log-dir=cache/log", + "--log-level=error", + "--password=encr-key", + "repository", + "status", + }, + }, + { + Name: "repository status with JSON output", + Command: func() (*safecli.Builder, error) { + args := StatusArgs{ + Common: common, + JSONOutput: true, + } + return Status(args) + }, + ExpectedCLI: []string{"kopia", + "--config-file=path/kopia.config", + "--log-dir=cache/log", + "--log-level=error", + "--password=encr-key", + "repository", + "status", + "--json", + }, + }, +})) diff --git a/repository/storage/azure/azure.go b/repository/storage/azure/azure.go new file mode 100644 index 0000000..d0959b3 --- /dev/null +++ b/repository/storage/azure/azure.go @@ -0,0 +1,31 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package azure + +import ( + "github.com/kanisterio/safecli/command" + + "github.com/kanisterio/safecli-kopia/internal" + "github.com/kanisterio/safecli-kopia/repository/storage/location" +) + +// New creates a new subcommand for the Azure storage. +func New(location location.Location, repoPathPrefix string) command.Applier { + prefix := internal.GenerateFullRepoPath(location.Prefix, repoPathPrefix) + return command.NewArguments(subcmdAzure, + optContainer(location.BucketName), + optPrefix(prefix), + ) +} diff --git a/repository/storage/azure/azure_opts.go b/repository/storage/azure/azure_opts.go new file mode 100644 index 0000000..b30be07 --- /dev/null +++ b/repository/storage/azure/azure_opts.go @@ -0,0 +1,39 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package azure + +import ( + "github.com/kanisterio/safecli/command" + + "github.com/kanisterio/safecli-kopia" +) + +var ( + subcmdAzure = command.NewArgument("azure") +) + +// optPrefix creates a new prefix option with a given prefix. +func optPrefix(prefix string) command.Applier { + return command.NewOptionWithArgument("--prefix", prefix) +} + +// optContainer creates a new container option with a given container name. +// If the name is empty, it returns ErrInvalidContainerName. +func optContainer(name string) command.Applier { + if name == "" { + return command.NewErrorArgument(cli.ErrInvalidContainerName) + } + return command.NewOptionWithArgument("--container", name) +} diff --git a/repository/storage/azure/azure_opts_test.go b/repository/storage/azure/azure_opts_test.go new file mode 100644 index 0000000..f8cfefc --- /dev/null +++ b/repository/storage/azure/azure_opts_test.go @@ -0,0 +1,45 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package azure + +import ( + "testing" + + "github.com/kanisterio/safecli/command" + "github.com/kanisterio/safecli/test" + "gopkg.in/check.v1" + + "github.com/kanisterio/safecli-kopia" +) + +func TestAzureOptions(t *testing.T) { check.TestingT(t) } + +var _ = check.Suite(&test.ArgumentSuite{Cmd: "cmd", Arguments: []test.ArgumentTest{ + { + Name: "optContainer", + Argument: optContainer("containername"), + ExpectedCLI: []string{"cmd", "--container=containername"}, + }, + { + Name: "optContainer with empty containername should return error", + Argument: optContainer(""), + ExpectedErr: cli.ErrInvalidContainerName, + }, + { + Name: "optPrefix", + Argument: command.NewArguments(optPrefix("prefix"), optPrefix("")), + ExpectedCLI: []string{"cmd", "--prefix=prefix", "--prefix="}, + }, +}}) diff --git a/repository/storage/azure/azure_test.go b/repository/storage/azure/azure_test.go new file mode 100644 index 0000000..46899c4 --- /dev/null +++ b/repository/storage/azure/azure_test.go @@ -0,0 +1,59 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package azure + +import ( + "testing" + + "github.com/kanisterio/safecli-kopia/repository/storage/location" + "github.com/kanisterio/safecli/command" + "github.com/kanisterio/safecli/test" + "gopkg.in/check.v1" + + cli "github.com/kanisterio/safecli-kopia" +) + +func TestNewAzure(t *testing.T) { check.TestingT(t) } + +func newAzure(prefix, repoPath, bucket string) command.Applier { + l := location.Location{ + Prefix: prefix, + BucketName: bucket, + } + return New(l, repoPath) +} + +var _ = check.Suite(&test.ArgumentSuite{Cmd: "cmd", Arguments: []test.ArgumentTest{ + { + Name: "NewAzure", + Argument: newAzure("prefix", "repoPath", "bucket"), + ExpectedCLI: []string{"cmd", "azure", "--container=bucket", "--prefix=prefix/repoPath/"}, + }, + { + Name: "NewAzure with empty repoPath", + Argument: newAzure("prefix", "", "bucket"), + ExpectedCLI: []string{"cmd", "azure", "--container=bucket", "--prefix=prefix/"}, + }, + { + Name: "NewAzure with empty local prefix and repo prefix should return error", + Argument: newAzure("", "", "bucket"), + ExpectedCLI: []string{"cmd", "azure", "--container=bucket", "--prefix="}, + }, + { + Name: "NewAzure with empty bucket should return ErrInvalidContainerName", + Argument: newAzure("", "", ""), + ExpectedErr: cli.ErrInvalidContainerName, + }, +}}) diff --git a/repository/storage/fs/fs.go b/repository/storage/fs/fs.go new file mode 100644 index 0000000..e1904e2 --- /dev/null +++ b/repository/storage/fs/fs.go @@ -0,0 +1,45 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fs + +import ( + "github.com/kanisterio/safecli/command" + + cli "github.com/kanisterio/safecli-kopia" + "github.com/kanisterio/safecli-kopia/internal" + "github.com/kanisterio/safecli-kopia/repository/storage/location" +) + +const ( + defaultFSMountPath = "/mnt/data" +) + +// New creates a new subcommand for the filesystem storage. +func New(location location.Location, repoPathPrefix string) command.Applier { + path, err := generateFileSystemMountPath(location.Prefix, repoPathPrefix) + if err != nil { + return command.NewErrorArgument(err) + } + return command.NewArguments(subcmdFilesystem, optRepoPath(path)) +} + +// generateFileSystemMountPath generates the mount path for the filesystem storage. +func generateFileSystemMountPath(locPrefix, repoPrefix string) (string, error) { + fullRepoPath := internal.GenerateFullRepoPath(locPrefix, repoPrefix) + if fullRepoPath == "" { + return "", cli.ErrInvalidRepoPath + } + return defaultFSMountPath + "/" + fullRepoPath, nil +} diff --git a/repository/storage/fs/fs_opts.go b/repository/storage/fs/fs_opts.go new file mode 100644 index 0000000..2b5ea61 --- /dev/null +++ b/repository/storage/fs/fs_opts.go @@ -0,0 +1,34 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fs + +import ( + "github.com/kanisterio/safecli/command" + + "github.com/kanisterio/safecli-kopia" +) + +var ( + subcmdFilesystem = command.NewArgument("filesystem") +) + +// optRepoPath creates a new path option with a given path. +// If the path is empty, it returns an error. +func optRepoPath(path string) command.Applier { + if path == "" { + return command.NewErrorArgument(cli.ErrInvalidRepoPath) + } + return command.NewOptionWithArgument("--path", path) +} diff --git a/repository/storage/fs/fs_opts_test.go b/repository/storage/fs/fs_opts_test.go new file mode 100644 index 0000000..ea58cdd --- /dev/null +++ b/repository/storage/fs/fs_opts_test.go @@ -0,0 +1,40 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fs + +import ( + "testing" + + "github.com/kanisterio/safecli/command" + "github.com/kanisterio/safecli/test" + "gopkg.in/check.v1" + + "github.com/kanisterio/safecli-kopia" +) + +func TestFilesystemOptions(t *testing.T) { check.TestingT(t) } + +var _ = check.Suite(&test.ArgumentSuite{Cmd: "cmd", Arguments: []test.ArgumentTest{ + { + Name: "optRepoPath", + Argument: command.NewArguments(optRepoPath("/path/to/repo")), + ExpectedCLI: []string{"cmd", "--path=/path/to/repo"}, + }, + { + Name: "Invalid RepoPath", + Argument: command.NewArguments(optRepoPath("")), + ExpectedErr: cli.ErrInvalidRepoPath, + }, +}}) diff --git a/repository/storage/fs/fs_test.go b/repository/storage/fs/fs_test.go new file mode 100644 index 0000000..1ba8b3a --- /dev/null +++ b/repository/storage/fs/fs_test.go @@ -0,0 +1,53 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fs + +import ( + "testing" + + "github.com/kanisterio/safecli-kopia/repository/storage/location" + "github.com/kanisterio/safecli/command" + "github.com/kanisterio/safecli/test" + "gopkg.in/check.v1" + + cli "github.com/kanisterio/safecli-kopia" +) + +func TestNewFilesystem(t *testing.T) { check.TestingT(t) } + +func newFilesystem(prefix, repoPath string) command.Applier { + l := location.Location{ + Prefix: prefix, + } + return New(l, repoPath) +} + +var _ = check.Suite(&test.ArgumentSuite{Cmd: "cmd", Arguments: []test.ArgumentTest{ + { + Name: "NewFilesystem", + Argument: newFilesystem("prefix", "repoPath"), + ExpectedCLI: []string{"cmd", "filesystem", "--path=/mnt/data/prefix/repoPath/"}, + }, + { + Name: "NewFilesystem with empty repoPath", + Argument: newFilesystem("prefix", ""), + ExpectedCLI: []string{"cmd", "filesystem", "--path=/mnt/data/prefix/"}, + }, + { + Name: "NewFilesystem with empty local prefix and repo prefix should return error", + Argument: newFilesystem("", ""), + ExpectedErr: cli.ErrInvalidRepoPath, + }, +}}) diff --git a/repository/storage/gcs/gcs.go b/repository/storage/gcs/gcs.go new file mode 100644 index 0000000..e08e76d --- /dev/null +++ b/repository/storage/gcs/gcs.go @@ -0,0 +1,32 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gcs + +import ( + "github.com/kanisterio/safecli/command" + + "github.com/kanisterio/safecli-kopia/internal" + "github.com/kanisterio/safecli-kopia/repository/storage/location" +) + +// New creates a new subcommand for the GCS storage. +func New(location location.Location, repoPathPrefix string) command.Applier { + prefix := internal.GenerateFullRepoPath(location.Prefix, repoPathPrefix) + return command.NewArguments(subcmdGCS, + optBucket(location.BucketName), + optCredentialsFile(location.CredentialsFile), + optPrefix(prefix), + ) +} diff --git a/repository/storage/gcs/gcs_opts.go b/repository/storage/gcs/gcs_opts.go new file mode 100644 index 0000000..72c8d70 --- /dev/null +++ b/repository/storage/gcs/gcs_opts.go @@ -0,0 +1,47 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gcs + +import ( + "github.com/kanisterio/safecli-kopia" + "github.com/kanisterio/safecli/command" +) + +var ( + subcmdGCS = command.NewArgument("gcs") +) + +// optBucket creates a new bucket option with a given name. +// If the name is empty, it returns ErrInvalidBucketName. +func optBucket(name string) command.Applier { + if name == "" { + return command.NewErrorArgument(cli.ErrInvalidBucketName) + } + return command.NewOptionWithArgument("--bucket", name) +} + +// optPrefix creates a new prefix option with a given prefix. +func optPrefix(prefix string) command.Applier { + return command.NewOptionWithArgument("--prefix", prefix) +} + +// optCredentialsFile creates a new GCS credentials file option with a given file path. +// If the file path is empty, it returns ErrInvalidCredentialsFile. +func optCredentialsFile(path string) command.Applier { + if path == "" { + return command.NewErrorArgument(cli.ErrInvalidCredentialsFile) + } + return command.NewOptionWithArgument("--credentials-file", path) +} diff --git a/repository/storage/gcs/gcs_opts_test.go b/repository/storage/gcs/gcs_opts_test.go new file mode 100644 index 0000000..b25642c --- /dev/null +++ b/repository/storage/gcs/gcs_opts_test.go @@ -0,0 +1,55 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gcs + +import ( + "testing" + + "gopkg.in/check.v1" + + "github.com/kanisterio/safecli-kopia" + "github.com/kanisterio/safecli/command" + "github.com/kanisterio/safecli/test" +) + +func TestGCSOptions(t *testing.T) { check.TestingT(t) } + +var _ = check.Suite(&test.ArgumentSuite{Cmd: "cmd", Arguments: []test.ArgumentTest{ + { + Name: "optBucket with bucketname should return option", + Argument: optBucket("bucketname"), + ExpectedCLI: []string{"cmd", "--bucket=bucketname"}, + }, + { + Name: "optBucket with empty bucketname should return error", + Argument: optBucket(""), + ExpectedErr: cli.ErrInvalidBucketName, + }, + { + Name: "optPrefix", + Argument: command.NewArguments(optPrefix("prefix"), optPrefix("")), + ExpectedCLI: []string{"cmd", "--prefix=prefix", "--prefix="}, + }, + { + Name: "optCredentialsFile with path should return option", + Argument: optCredentialsFile("/tmp/file.creds"), + ExpectedCLI: []string{"cmd", "--credentials-file=/tmp/file.creds"}, + }, + { + Name: "optCredentialsFile with empty path should return error", + Argument: optCredentialsFile(""), + ExpectedErr: cli.ErrInvalidCredentialsFile, + }, +}}) diff --git a/repository/storage/gcs/gcs_test.go b/repository/storage/gcs/gcs_test.go new file mode 100644 index 0000000..0ddd358 --- /dev/null +++ b/repository/storage/gcs/gcs_test.go @@ -0,0 +1,60 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gcs + +import ( + "testing" + + "github.com/kanisterio/safecli-kopia/repository/storage/location" + "github.com/kanisterio/safecli/command" + "github.com/kanisterio/safecli/test" + "gopkg.in/check.v1" + + cli "github.com/kanisterio/safecli-kopia" +) + +func TestNewGCS(t *testing.T) { check.TestingT(t) } + +func newGCS(prefix, repoPath, bucket string) command.Applier { + l := location.Location{ + Prefix: prefix, + BucketName: bucket, + CredentialsFile: "/tmp/creds.txt", + } + return New(l, repoPath) +} + +var _ = check.Suite(&test.ArgumentSuite{Cmd: "cmd", Arguments: []test.ArgumentTest{ + { + Name: "NewGCS", + Argument: newGCS("prefix", "repoPath", "bucket"), + ExpectedCLI: []string{"cmd", "gcs", "--bucket=bucket", "--credentials-file=/tmp/creds.txt", "--prefix=prefix/repoPath/"}, + }, + { + Name: "NewGCS with empty repoPath", + Argument: newGCS("prefix", "", "bucket"), + ExpectedCLI: []string{"cmd", "gcs", "--bucket=bucket", "--credentials-file=/tmp/creds.txt", "--prefix=prefix/"}, + }, + { + Name: "NewGCS with empty local prefix and repo prefix should return error", + Argument: newGCS("", "", "bucket"), + ExpectedCLI: []string{"cmd", "gcs", "--bucket=bucket", "--credentials-file=/tmp/creds.txt", "--prefix="}, + }, + { + Name: "NewGCS with empty bucket should return ErrInvalidBucketName", + Argument: newGCS("", "", ""), + ExpectedErr: cli.ErrInvalidBucketName, + }, +}}) diff --git a/repository/storage/location/location.go b/repository/storage/location/location.go new file mode 100644 index 0000000..585fc55 --- /dev/null +++ b/repository/storage/location/location.go @@ -0,0 +1,36 @@ +package location + +import "strings" + +type LocType string + +const ( + LocTypeS3 LocType = "s3" + LocTypes3Compliant LocType = "s3Compliant" + LocTypeGCS LocType = "gcs" + LocTypeAzure LocType = "azure" + LocTypeFilestore LocType = "filestore" +) + +type Location struct { + Type LocType + Region string + BucketName string + Endpoint string + Prefix string + HasSkipSSLVerify bool + CredentialsFile string +} + +func (l Location) IsInsecureEndpoint() bool { + return strings.HasPrefix(l.Endpoint, "http:") +} + +func (l Location) IsPointInTypeSupported() bool { + switch l.Type { + case LocTypeAzure, LocTypeS3, LocTypes3Compliant: + return true + default: + return false + } +} diff --git a/repository/storage/location/location_test.go b/repository/storage/location/location_test.go new file mode 100644 index 0000000..42e3322 --- /dev/null +++ b/repository/storage/location/location_test.go @@ -0,0 +1,82 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package location_test + +import ( + "testing" + + "gopkg.in/check.v1" + + "github.com/kanisterio/safecli-kopia/repository/storage/location" +) + +func TestLocation(t *testing.T) { check.TestingT(t) } + +type LocationSuite struct{} + +var _ = check.Suite(&LocationSuite{}) + +func (s *LocationSuite) TestIsInsecureEndpoint(c *check.C) { + insecure_location := location.Location{ + Endpoint: "http://example.com", + } + secure_location := location.Location{ + Endpoint: "https://example.com", + } + + c.Assert(insecure_location.IsInsecureEndpoint(), check.Equals, true) + c.Assert(secure_location.IsInsecureEndpoint(), check.Equals, false) +} + +func (s *LocationSuite) TestIsPointInTypeSupported(c *check.C) { + tests := []struct { + location location.Location + supported bool + }{ + { + location: location.Location{ + Type: location.LocTypeAzure, + }, + supported: true, + }, + { + location: location.Location{ + Type: location.LocTypeS3, + }, + supported: true, + }, + { + location: location.Location{ + Type: location.LocTypes3Compliant, + }, + supported: true, + }, + { + location: location.Location{ + Type: location.LocTypeGCS, + }, + supported: false, + }, + { + location: location.Location{ + Type: location.LocTypeFilestore, + }, + supported: false, + }, + } + for _, test := range tests { + c.Check(test.location.IsPointInTypeSupported(), check.Equals, test.supported, check.Commentf("%s", test.location.Type)) + } +} diff --git a/repository/storage/s3/s3.go b/repository/storage/s3/s3.go new file mode 100644 index 0000000..26b52c4 --- /dev/null +++ b/repository/storage/s3/s3.go @@ -0,0 +1,55 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package s3 + +import ( + "strings" + + "github.com/kanisterio/safecli/command" + + "github.com/kanisterio/safecli-kopia/internal" + "github.com/kanisterio/safecli-kopia/repository/storage/location" +) + +// New creates a new subcommand for the S3 storage. +func New(location location.Location, repoPathPrefix string) command.Applier { + endpoint := resolveS3Endpoint(location.Endpoint) + prefix := internal.GenerateFullRepoPath(location.Prefix, repoPathPrefix) + return command.NewArguments(subcmdS3, + optRegion(location.Region), + optBucket(location.BucketName), + optEndpoint(endpoint), + optPrefix(prefix), + optDisableTLS(location.IsInsecureEndpoint()), + optDisableTLSVerify(location.HasSkipSSLVerify), + ) +} + +// resolveS3Endpoint removes the trailing slash and +// protocol from provided endpoint and +// returns the absolute endpoint string. +func resolveS3Endpoint(endpoint string) string { + if endpoint == "" { + return "" + } + + if strings.HasSuffix(endpoint, "/") { + endpoint = strings.TrimRight(endpoint, "/") + } + + sp := strings.SplitN(endpoint, "://", 2) + + return sp[len(sp)-1] +} diff --git a/repository/storage/s3/s3_opts.go b/repository/storage/s3/s3_opts.go new file mode 100644 index 0000000..b31b01a --- /dev/null +++ b/repository/storage/s3/s3_opts.go @@ -0,0 +1,68 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package s3 + +import ( + "github.com/kanisterio/safecli/command" + + "github.com/kanisterio/safecli-kopia" +) + +var ( + subcmdS3 = command.NewArgument("s3") +) + +// optBucket creates a new bucket option with a given name. +// If the name is empty, it returns ErrInvalidBucketName. +func optBucket(name string) command.Applier { + if name == "" { + return command.NewErrorArgument(cli.ErrInvalidBucketName) + } + return command.NewOptionWithArgument("--bucket", name) +} + +// optEndpoint creates a new endpoint option with a given endpoint. +// If the endpoint is empty, the endpoint option is not set. +func optEndpoint(endpoint string) command.Applier { + if endpoint == "" { + return command.NewNoopArgument() + } + return command.NewOptionWithArgument("--endpoint", endpoint) +} + +// optPrefix creates a new prefix option with a given prefix. +// If the prefix is empty, the prefix option is not set. +func optPrefix(prefix string) command.Applier { + return command.NewOptionWithArgument("--prefix", prefix) +} + +// optRegion creates a new region option with a given region. +// If the region is empty, the region option is not set. +func optRegion(region string) command.Applier { + if region == "" { + return command.NewNoopArgument() + } + return command.NewOptionWithArgument("--region", region) +} + +// optDisableTLS creates a new disable TLS option with a given value. +func optDisableTLS(disable bool) command.Applier { + return command.NewOption("--disable-tls", disable) +} + +// optDisableTLSVerify creates a new disable TLS verification option with a given value. +func optDisableTLSVerify(disable bool) command.Applier { + return command.NewOption("--disable-tls-verification", disable) +} diff --git a/repository/storage/s3/s3_opts_test.go b/repository/storage/s3/s3_opts_test.go new file mode 100644 index 0000000..c11a013 --- /dev/null +++ b/repository/storage/s3/s3_opts_test.go @@ -0,0 +1,65 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package s3 + +import ( + "testing" + + "github.com/kanisterio/safecli/command" + "github.com/kanisterio/safecli/test" + "gopkg.in/check.v1" + + "github.com/kanisterio/safecli-kopia" +) + +func TestS3Options(t *testing.T) { check.TestingT(t) } + +var _ = check.Suite(&test.ArgumentSuite{Cmd: "cmd", Arguments: []test.ArgumentTest{ + { + Name: "optRegion", + Argument: command.NewArguments(optRegion("region"), optRegion("")), + ExpectedCLI: []string{"cmd", "--region=region"}, + }, + { + Name: "optBucket with bucketname should return option", + Argument: optBucket("bucketname"), + ExpectedCLI: []string{"cmd", "--bucket=bucketname"}, + }, + { + Name: "optBucket with empty bucketname should return error", + Argument: optBucket(""), + ExpectedErr: cli.ErrInvalidBucketName, + }, + { + Name: "optEndpoint", + Argument: command.NewArguments(optEndpoint("endpoint"), optEndpoint("")), + ExpectedCLI: []string{"cmd", "--endpoint=endpoint"}, + }, + { + Name: "optPrefix", + Argument: command.NewArguments(optPrefix("prefix"), optPrefix("")), + ExpectedCLI: []string{"cmd", "--prefix=prefix", "--prefix="}, + }, + { + Name: "optDisableTLS", + Argument: command.NewArguments(optDisableTLS(true), optDisableTLS(false)), + ExpectedCLI: []string{"cmd", "--disable-tls"}, + }, + { + Name: "optDisableTLSVerify", + Argument: command.NewArguments(optDisableTLSVerify(true), optDisableTLSVerify(false)), + ExpectedCLI: []string{"cmd", "--disable-tls-verification"}, + }, +}}) diff --git a/repository/storage/s3/s3_test.go b/repository/storage/s3/s3_test.go new file mode 100644 index 0000000..53b015d --- /dev/null +++ b/repository/storage/s3/s3_test.go @@ -0,0 +1,137 @@ +// Copyright 2024 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package s3 + +import ( + "testing" + + "github.com/kanisterio/safecli/test" + "gopkg.in/check.v1" + + cli "github.com/kanisterio/safecli-kopia" + "github.com/kanisterio/safecli-kopia/repository/storage/location" +) + +func TestNewS3(t *testing.T) { check.TestingT(t) } + +func newLocation(prefix, endpoint, region, bucket string, skipSSLVerify bool) location.Location { + return location.Location{ + Prefix: prefix, + Endpoint: endpoint, + Region: region, + BucketName: bucket, + HasSkipSSLVerify: skipSSLVerify, + } +} + +// s3test is a test case for NewS3. +type s3test struct { + Name string + Location location.Location + RepoPath string + ExpectedCLI []string + ExpectedErr error +} + +// newS3Test creates a new test case for NewS3. +func newS3Test(s3t s3test) test.ArgumentTest { + return test.ArgumentTest{ + Name: s3t.Name, + Argument: New(s3t.Location, s3t.RepoPath), + ExpectedCLI: s3t.ExpectedCLI, + ExpectedErr: s3t.ExpectedErr, + } +} + +// toArgTests converts a list of s3tests to a list of ArgumentTests. +func toArgTests(s3tests []s3test) []test.ArgumentTest { + argTests := make([]test.ArgumentTest, len(s3tests)) + for i, s3t := range s3tests { + argTests[i] = newS3Test(s3t) + } + return argTests +} + +var _ = check.Suite(&test.ArgumentSuite{Cmd: "cmd", Arguments: toArgTests([]s3test{ + { + Name: "NewS3", + Location: newLocation("prefix", "http://endpoint/path/", "region", "bucket", true), + RepoPath: "repoPath", + ExpectedCLI: []string{"cmd", "s3", + "--region=region", + "--bucket=bucket", + "--endpoint=endpoint/path", + "--prefix=prefix/repoPath/", + "--disable-tls", + "--disable-tls-verification", + }, + }, + { + Name: "NewS3 w/o logger should not panic", + Location: newLocation("prefix", "http://endpoint/path/", "region", "bucket", true), + RepoPath: "repoPath", + ExpectedCLI: []string{"cmd", "s3", + "--region=region", + "--bucket=bucket", + "--endpoint=endpoint/path", + "--prefix=prefix/repoPath/", + "--disable-tls", + "--disable-tls-verification", + }, + }, + { + Name: "NewS3 with empty repoPath and https endpoint", + Location: newLocation("prefix", "https://endpoint/path/", "region", "bucket", false), + ExpectedCLI: []string{"cmd", "s3", + "--region=region", + "--bucket=bucket", + "--endpoint=endpoint/path", + "--prefix=prefix/", + }, + }, + { + Name: "NewS3 with empty repoPath and endpoint", + Location: newLocation("prefix", "", "region", "bucket", true), + ExpectedCLI: []string{"cmd", "s3", + "--region=region", + "--bucket=bucket", + "--prefix=prefix/", + "--disable-tls-verification", + }, + }, + { + Name: "NewS3 with empty repoPath, prefix and endpoint", + Location: newLocation("", "", "region", "bucket", true), + ExpectedCLI: []string{"cmd", "s3", + "--region=region", + "--bucket=bucket", + "--prefix=", + "--disable-tls-verification", + }, + }, + { + Name: "NewS3 with empty repoPath, prefix, endpoint and bucket", + ExpectedErr: cli.ErrInvalidBucketName, + }, + { + Name: "NewS3 with empty logger should not panic", + Location: newLocation("", "https://endpoint/path/", "", "bucket", false), + ExpectedCLI: []string{"cmd", "s3", + "--bucket=bucket", + "--endpoint=endpoint/path", + "--prefix=", + }, + }, +})})