Add validation for plugin name format and dups (#1339)

* Add validation for plugin name format and dups

Signed-off-by: Carlisia <carlisiac@vmware.com>

* Bug fix

Signed-off-by: Carlisia <carlisiac@vmware.com>

* Add changelog

Signed-off-by: Carlisia <carlisiac@vmware.com>

* Address code reviews

Signed-off-by: Carlisia <carlisiac@vmware.com>

* Fix code

Signed-off-by: Carlisia <carlisiac@vmware.com>

* Address code reviews

Signed-off-by: Carlisia <carlisiac@vmware.com>

* Add documentation

Signed-off-by: Carlisia <carlisiac@vmware.com>

* Update godoc

Signed-off-by: Carlisia <carlisiac@vmware.com>

* More doc

Signed-off-by: Carlisia <carlisiac@vmware.com>
This commit is contained in:
KubeKween
2019-04-12 08:25:04 -06:00
committed by Steve Kriss
parent 0e0f357cef
commit abee09aa2d
9 changed files with 167 additions and 35 deletions
+1
View File
@@ -0,0 +1 @@
Validate that there can't be any duplicate plugin name, and that the name format is `example.io/name`.
+18 -5
View File
@@ -1,15 +1,28 @@
# Plugins
Velero has a plugin architecture that allows users to add their own custom functionality to Velero backups & restores
without having to modify/recompile the core Velero binary. To add custom functionality, users simply create their own binary
containing implementations of Velero's plugin kinds (described below), plus a small amount of boilerplate code to
expose the plugin implementations to Velero. This binary is added to a container image that serves as an init container for
the Velero server pod and copies the binary into a shared emptyDir volume for the Velero server to access.
Velero has a plugin architecture that allows users to add their own custom functionality to Velero backups & restores without having to modify/recompile the core Velero binary. To add custom functionality, users simply create their own binary containing implementations of Velero's plugin kinds (described below), plus a small amount of boilerplate code to expose the plugin implementations to Velero. This binary is added to a container image that serves as an init container for the Velero server pod and copies the binary into a shared emptyDir volume for the Velero server to access.
Multiple plugins, of any type, can be implemented in this binary.
A fully-functional [sample plugin repository][1] is provided to serve as a convenient starting point for plugin authors.
## Plugin Naming
When naming your plugin, keep in mind that the name needs to conform to these rules:
- have two parts separated by '/'
- none of the above parts can be empty
- the prefix is a valid DNS subdomain name
- a plugin with the same name cannot not already exist
### Some examples:
```
- example.io/azure
- 1.2.3.4/5678
- example-with-dash.io/azure
```
You will need to give your plugin(s) a name when registering them by calling the appropriate `RegisterX` function: <https://github.com/heptio/velero/blob/0e0f357cef7cf15d4c1d291d3caafff2eeb69c1e/pkg/plugin/framework/server.go#L42-L60>
## Plugin Kinds
Velero currently supports the following kinds of plugins:
+16 -16
View File
@@ -38,22 +38,22 @@ func NewCommand(f client.Factory) *cobra.Command {
Short: "INTERNAL COMMAND ONLY - not intended to be run directly by users",
Run: func(c *cobra.Command, args []string) {
pluginServer.
RegisterObjectStore("aws", newAwsObjectStore).
RegisterObjectStore("azure", newAzureObjectStore).
RegisterObjectStore("gcp", newGcpObjectStore).
RegisterVolumeSnapshotter("aws", newAwsVolumeSnapshotter).
RegisterVolumeSnapshotter("azure", newAzureVolumeSnapshotter).
RegisterVolumeSnapshotter("gcp", newGcpVolumeSnapshotter).
RegisterBackupItemAction("pv", newPVBackupItemAction).
RegisterBackupItemAction("pod", newPodBackupItemAction).
RegisterBackupItemAction("serviceaccount", newServiceAccountBackupItemAction(f)).
RegisterRestoreItemAction("job", newJobRestoreItemAction).
RegisterRestoreItemAction("pod", newPodRestoreItemAction).
RegisterRestoreItemAction("restic", newResticRestoreItemAction(f)).
RegisterRestoreItemAction("service", newServiceRestoreItemAction).
RegisterRestoreItemAction("serviceaccount", newServiceAccountRestoreItemAction).
RegisterRestoreItemAction("addPVCFromPod", newAddPVCFromPodRestoreItemAction).
RegisterRestoreItemAction("addPVFromPVC", newAddPVFromPVCRestoreItemAction).
RegisterObjectStore("velero.io/aws", newAwsObjectStore).
RegisterObjectStore("velero.io/azure", newAzureObjectStore).
RegisterObjectStore("velero.io/gcp", newGcpObjectStore).
RegisterVolumeSnapshotter("velero.io/aws", newAwsVolumeSnapshotter).
RegisterVolumeSnapshotter("velero.io/azure", newAzureVolumeSnapshotter).
RegisterVolumeSnapshotter("velero.io/gcp", newGcpVolumeSnapshotter).
RegisterBackupItemAction("velero.io/pv", newPVBackupItemAction).
RegisterBackupItemAction("velero.io/pod", newPodBackupItemAction).
RegisterBackupItemAction("velero.io/serviceaccount", newServiceAccountBackupItemAction(f)).
RegisterRestoreItemAction("velero.io/job", newJobRestoreItemAction).
RegisterRestoreItemAction("velero.io/pod", newPodRestoreItemAction).
RegisterRestoreItemAction("velero.io/restic", newResticRestoreItemAction(f)).
RegisterRestoreItemAction("velero.io/service", newServiceRestoreItemAction).
RegisterRestoreItemAction("velero.io/serviceaccount", newServiceAccountRestoreItemAction).
RegisterRestoreItemAction("velero.io/addPVCFromPod", newAddPVCFromPodRestoreItemAction).
RegisterRestoreItemAction("velero.io/addPVFromPVC", newAddPVFromPVCRestoreItemAction).
Serve()
},
}
+5
View File
@@ -202,6 +202,11 @@ func (r *registry) register(id framework.PluginIdentifier) error {
return newDuplicatePluginRegistrationError(existing, id)
}
// no need to pass list of existing plugins since the check if this exists was done above
if err := framework.ValidatePluginName(id.Name, nil); err != nil {
return errors.Errorf("invalid plugin name %q: %s", id.Name, err)
}
r.pluginsByID[key] = id
r.pluginsByKind[id.Kind] = append(r.pluginsByKind[id.Kind], id)
+2 -2
View File
@@ -23,8 +23,8 @@ import (
func ExampleNewServer_volumeSnapshotter() {
NewServer(). // call the server
RegisterVolumeSnapshotter("example-volumesnapshotter", newVolumeSnapshotter). // register the plugin
Serve() // serve the plugin
RegisterVolumeSnapshotter("example.io/volumesnapshotter", newVolumeSnapshotter). // register the plugin with a valid name
Serve() // serve the plugin
}
func newVolumeSnapshotter(logger logrus.FieldLogger) (interface{}, error) {
+12 -8
View File
@@ -38,26 +38,30 @@ type Server interface {
// This method must be called prior to calling .Serve().
BindFlags(flags *pflag.FlagSet) Server
// RegisterBackupItemAction registers a backup item action.
RegisterBackupItemAction(name string, initializer HandlerInitializer) Server
// RegisterBackupItemAction registers a backup item action. Accepted format
// for the plugin name is <DNS subdomain>/<non-empty name>.
RegisterBackupItemAction(pluginName string, initializer HandlerInitializer) Server
// RegisterBackupItemActions registers multiple backup item actions.
RegisterBackupItemActions(map[string]HandlerInitializer) Server
// RegisterVolumeSnapshotter registers a volume snapshotter.
RegisterVolumeSnapshotter(name string, initializer HandlerInitializer) Server
// RegisterVolumeSnapshotter registers a volume snapshotter. Accepted format
// for the plugin name is <DNS subdomain>/<non-empty name>.
RegisterVolumeSnapshotter(pluginName string, initializer HandlerInitializer) Server
// RegisterVolumeSnapshotters registers multiple volume snapshotters.
RegisterVolumeSnapshotters(map[string]HandlerInitializer) Server
// RegisterObjectStore registers an object store.
RegisterObjectStore(name string, initializer HandlerInitializer) Server
// RegisterObjectStore registers an object store. Accepted format
// for the plugin name is <DNS subdomain>/<non-empty name>.
RegisterObjectStore(pluginName string, initializer HandlerInitializer) Server
// RegisterObjectStores registers multiple object stores.
RegisterObjectStores(map[string]HandlerInitializer) Server
// RegisterRestoreItemAction registers a restore item action.
RegisterRestoreItemAction(name string, initializer HandlerInitializer) Server
// RegisterRestoreItemAction registers a restore item action. Accepted format
// for the plugin name is <DNS subdomain>/<non-empty name>.
RegisterRestoreItemAction(pluginName string, initializer HandlerInitializer) Server
// RegisterRestoreItemActions registers multiple restore item actions.
RegisterRestoreItemActions(map[string]HandlerInitializer) Server
+1 -1
View File
@@ -52,7 +52,7 @@ func newGRPCErrorWithCode(err error, code codes.Code, details ...goproto.Message
return statusErr.Err()
}
// newGRPCError is a convenience functino for creating a new gRPC error
// newGRPCError is a convenience function for creating a new gRPC error
// with code = codes.Unknown
func newGRPCError(err error, details ...goproto.Message) error {
return newGRPCErrorWithCode(err, codes.Unknown, details...)
+41 -3
View File
@@ -17,9 +17,12 @@ limitations under the License.
package framework
import (
"strings"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation"
)
// HandlerInitializer is a function that initializes and returns a new instance of one of Velero's plugin interfaces
@@ -43,9 +46,13 @@ func newServerMux(logger logrus.FieldLogger) *serverMux {
}
}
// register registers the initializer for name.
// register validates the plugin name and registers the
// initializer for the given name.
func (m *serverMux) register(name string, f HandlerInitializer) {
// TODO(ncdc): return an error on duplicate registrations for the same name.
if err := ValidatePluginName(name, m.names()); err != nil {
m.serverLog.Errorf("invalid plugin name %q: %s", name, err)
return
}
m.initializers[name] = f
}
@@ -63,7 +70,7 @@ func (m *serverMux) getHandler(name string) (interface{}, error) {
initializer, found := m.initializers[name]
if !found {
return nil, errors.Errorf("unknown %v plugin: %s", m.kind, name)
return nil, errors.Errorf("%v plugin: %s was not found or has an invalid name format", m.kind, name)
}
instance, err := initializer(m.serverLog)
@@ -75,3 +82,34 @@ func (m *serverMux) getHandler(name string) (interface{}, error) {
return m.handlers[name], nil
}
// ValidatePluginName checks if the given name:
// - the plugin name has two parts separated by '/'
// - non of the above parts is empty
// - the prefix is a valid DNS subdomain name
// - a plugin with the same name does not already exist (if list of existing names is passed in)
func ValidatePluginName(name string, existingNames []string) error {
// validate there is one "/" and two parts
parts := strings.Split(name, "/")
if len(parts) != 2 {
return errors.Errorf("plugin name must have exactly two parts separated by a `/`. Accepted format: <DNS subdomain>/<non-empty name>. %s is invalid", name)
}
// validate both prefix and name are non-empty
if parts[0] == "" || parts[1] == "" {
return errors.Errorf("both parts of the plugin name must be non-empty. Accepted format: <DNS subdomain>/<non-empty name>. %s is invalid", name)
}
// validate that the prefix is a DNS subdomain
if errs := validation.IsDNS1123Subdomain(parts[0]); len(errs) != 0 {
return errors.Errorf("first part of the plugin name must be a valid DNS subdomain. Accepted format: <DNS subdomain>/<non-empty name>. first part %q is invalid: %s", parts[0], strings.Join(errs, "; "))
}
for _, existingName := range existingNames {
if strings.Compare(name, existingName) == 0 {
return errors.New("plugin name " + existingName + " already exists")
}
}
return nil
}
+71
View File
@@ -0,0 +1,71 @@
/*
Copyright 2019 the Velero contributors.
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 framework
import (
"strings"
"testing"
)
func TestValidPluginName(t *testing.T) {
successCases := []struct {
pluginName string
existingNames []string
}{
{"example.io/azure", []string{"velero.io/aws"}},
{"with-dashes/name", []string{"velero.io/aws"}},
{"prefix/Uppercase_Is_OK_123", []string{"velero.io/aws"}},
{"example-with-dash.io/azure", []string{"velero.io/aws"}},
{"1.2.3.4/5678", []string{"velero.io/aws"}},
{"example.io/azure", []string{"velero.io/aws"}},
{"example.io/azure", []string{""}},
{"example.io/azure", nil},
{strings.Repeat("a", 253) + "/name", []string{"velero.io/aws"}},
}
for i, tt := range successCases {
t.Run(tt.pluginName, func(t *testing.T) {
if err := ValidatePluginName(tt.pluginName, tt.existingNames); err != nil {
t.Errorf("case[%d]: %q: expected success: %v", i, successCases[i], err)
}
})
}
errorCases := []struct {
pluginName string
existingNames []string
}{
{"", []string{"velero.io/aws"}},
{"single", []string{"velero.io/aws"}},
{"/", []string{"velero.io/aws"}},
{"//", []string{"velero.io/aws"}},
{"///", []string{"velero.io/aws"}},
{"a/", []string{"velero.io/aws"}},
{"/a", []string{"velero.io/aws"}},
{"velero.io/aws", []string{"velero.io/aws"}},
{"Uppercase_Is_OK_123/name", []string{"velero.io/aws"}},
{strings.Repeat("a", 254) + "/name", []string{"velero.io/aws"}},
{"ospecialchars%^=@", []string{"velero.io/aws"}},
}
for i, tt := range errorCases {
t.Run(tt.pluginName, func(t *testing.T) {
if err := ValidatePluginName(tt.pluginName, tt.existingNames); err == nil {
t.Errorf("case[%d]: %q: expected failure.", i, errorCases[i])
}
})
}
}