Preserve nodePort support with --preserve-nodeports flag (#3095)

* -> Preserve nodePort support when restoring via "--preserve-nodeports" flag

Signed-off-by: Yusuf Güngör <yusuf.gungor@hepsiburada.com>

* -> Added changelog.

Signed-off-by: Yusuf Güngör <yusuf.gungor@hepsiburada.com>

* -> Unit test added.
-> Using boolptr.IsSetToTrue for bool ptr check.

Signed-off-by: Yusuf Güngör <yusuf.gungor@hepsiburada.com>

* -> Unit test added.
-> Using boolptr.IsSetToTrue for bool ptr check.

Signed-off-by: Yusuf Güngör <yusuf.gungor@hepsiburada.com>

* -> Other restore errors log level changed from info to error.
-> Documentation updated about Velero nodePort restore logic and preservation of them.

Signed-off-by: Yusuf Güngör <yusuf.gungor@hepsiburada.com>

Co-authored-by: Yusuf Güngör <yusuf.gungor@hepsiburada.com>
This commit is contained in:
yusufgungor
2020-12-09 09:32:34 -08:00
committed by GitHub
co-authored by Yusuf Güngör
parent d09b4d60bb
commit 3b2e9036d1
16 changed files with 203 additions and 5 deletions
+1
View File
@@ -0,0 +1 @@
Added "--preserve-nodeports" flag to preserve original nodePorts when restoring.
+5
View File
@@ -1574,6 +1574,11 @@ spec:
target namespace names to restore into. Any source namespaces not
included in the map will be restored into namespaces of the same name.
type: object
preserveNodePorts:
description: PreserveNodePorts specifies whether to restore old nodePorts
from backup.
nullable: true
type: boolean
restorePVs:
description: RestorePVs specifies whether to restore all included PVs
from snapshot (via the cloudprovider).
File diff suppressed because one or more lines are too long
+4
View File
@@ -679,6 +679,10 @@ spec:
PVs from snapshot (via the cloudprovider).
nullable: true
type: boolean
preserveNodePorts:
description: PreserveNodePorts specifies whether to restore old nodePorts from backup.
nullable: true
type: boolean
scheduleName:
description: ScheduleName is the unique name of the Velero schedule
to restore from. If specified, and BackupName is empty, Velero will
+5
View File
@@ -77,6 +77,11 @@ type RestoreSpec struct {
// +nullable
RestorePVs *bool `json:"restorePVs,omitempty"`
// PreserveNodePorts specifies whether to restore old nodePorts from backup.
// +optional
// +nullable
PreserveNodePorts *bool `json:"preserveNodePorts,omitempty"`
// IncludeClusterResources specifies whether cluster-scoped resources
// should be included for consideration in the restore. If null, defaults
// to true.
@@ -1272,6 +1272,11 @@ func (in *RestoreSpec) DeepCopyInto(out *RestoreSpec) {
*out = new(bool)
**out = **in
}
if in.PreserveNodePorts != nil {
in, out := &in.PreserveNodePorts, &out.PreserveNodePorts
*out = new(bool)
**out = **in
}
if in.IncludeClusterResources != nil {
in, out := &in.IncludeClusterResources, &out.IncludeClusterResources
*out = new(bool)
+6
View File
@@ -136,6 +136,12 @@ func (b *RestoreBuilder) RestorePVs(val bool) *RestoreBuilder {
return b
}
// PreserveNodePorts sets the Restore's preserved NodePorts.
func (b *RestoreBuilder) PreserveNodePorts(val bool) *RestoreBuilder {
b.object.Spec.PreserveNodePorts = &val
return b
}
// StartTimestamp sets the Restore's start timestamp.
func (b *RestoreBuilder) StartTimestamp(val time.Time) *RestoreBuilder {
b.object.Status.StartTimestamp = &metav1.Time{Time: val}
+8
View File
@@ -78,6 +78,7 @@ type CreateOptions struct {
ScheduleName string
RestoreName string
RestoreVolumes flag.OptionalBool
PreserveNodePorts flag.OptionalBool
Labels flag.Map
IncludeNamespaces flag.StringArray
ExcludeNamespaces flag.StringArray
@@ -98,6 +99,7 @@ func NewCreateOptions() *CreateOptions {
IncludeNamespaces: flag.NewStringArray("*"),
NamespaceMappings: flag.NewMap().WithEntryDelimiter(",").WithKeyValueDelimiter(":"),
RestoreVolumes: flag.NewOptionalBool(nil),
PreserveNodePorts: flag.NewOptionalBool(nil),
IncludeClusterResources: flag.NewOptionalBool(nil),
}
}
@@ -117,6 +119,11 @@ func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) {
// like a normal bool flag
f.NoOptDefVal = "true"
f = flags.VarPF(&o.PreserveNodePorts, "preserve-nodeports", "", "Whether to preserve nodeports of Services when restoring.")
// this allows the user to just specify "--preserve-nodeports" as shorthand for "--preserve-nodeports=true"
// like a normal bool flag
f.NoOptDefVal = "true"
f = flags.VarPF(&o.IncludeClusterResources, "include-cluster-resources", "", "Include cluster-scoped resources in the restore.")
f.NoOptDefVal = "true"
@@ -260,6 +267,7 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error {
NamespaceMapping: o.NamespaceMappings.Data(),
LabelSelector: o.Selector.LabelSelector,
RestorePVs: o.RestoreVolumes.Value,
PreserveNodePorts: o.PreserveNodePorts.Value,
IncludeClusterResources: o.IncludeClusterResources.Value,
},
}
+4
View File
@@ -127,6 +127,10 @@ func DescribeRestore(restore *v1.Restore, podVolumeRestores []v1.PodVolumeRestor
d.Println()
describePodVolumeRestores(d, podVolumeRestores, details)
}
d.Println()
d.Printf("Preserve Service NodePorts:\t%s\n", BoolPointerString(restore.Spec.PreserveNodePorts, "false", "true", "auto"))
})
}
+1 -1
View File
@@ -1167,7 +1167,7 @@ func (ctx *restoreContext) restoreItem(obj *unstructured.Unstructured, groupReso
// Error was something other than an AlreadyExists
if restoreErr != nil {
ctx.log.Infof("error restoring %s: %v", name, restoreErr)
ctx.log.Errorf("error restoring %s: %+v", name, restoreErr)
errs.Add(namespace, fmt.Errorf("error restoring %s: %v", resourceID, restoreErr))
return warnings, errs
}
+8 -2
View File
@@ -27,6 +27,7 @@ import (
"k8s.io/apimachinery/pkg/util/sets"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
)
const annotationLastAppliedConfig = "kubectl.kubernetes.io/last-applied-configuration"
@@ -55,8 +56,13 @@ func (a *ServiceAction) Execute(input *velero.RestoreItemActionExecuteInput) (*v
service.Spec.ClusterIP = ""
}
if err := deleteNodePorts(service); err != nil {
return nil, err
/* Do not delete NodePorts if restore triggered with "--preserve-nodeports" flag */
if boolptr.IsSetToTrue(input.Restore.Spec.PreserveNodePorts) {
a.log.Info("Restoring Services with original NodePort(s)")
} else {
if err := deleteNodePorts(service); err != nil {
return nil, err
}
}
res, err := runtime.DefaultUnstructuredConverter.ToUnstructured(service)
+51 -1
View File
@@ -27,6 +27,8 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/builder"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
velerotest "github.com/vmware-tanzu/velero/pkg/test"
)
@@ -51,6 +53,7 @@ func TestServiceActionExecute(t *testing.T) {
tests := []struct {
name string
obj corev1api.Service
restore *api.Restore
expectedErr bool
expectedRes corev1api.Service
}{
@@ -65,6 +68,7 @@ func TestServiceActionExecute(t *testing.T) {
LoadBalancerIP: "should-be-kept",
},
},
restore: builder.ForRestore(api.DefaultNamespace, "").Result(),
expectedErr: false,
expectedRes: corev1api.Service{
ObjectMeta: metav1.ObjectMeta{
@@ -85,6 +89,7 @@ func TestServiceActionExecute(t *testing.T) {
ClusterIP: "None",
},
},
restore: builder.ForRestore(api.DefaultNamespace, "").Result(),
expectedRes: corev1api.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "svc-1",
@@ -113,6 +118,7 @@ func TestServiceActionExecute(t *testing.T) {
},
},
},
restore: builder.ForRestore(api.DefaultNamespace, "").Result(),
expectedRes: corev1api.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "svc-1",
@@ -146,6 +152,7 @@ func TestServiceActionExecute(t *testing.T) {
},
},
},
restore: builder.ForRestore(api.DefaultNamespace, "").Result(),
expectedRes: corev1api.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "svc-1",
@@ -177,6 +184,7 @@ func TestServiceActionExecute(t *testing.T) {
},
},
},
restore: builder.ForRestore(api.DefaultNamespace, "").Result(),
expectedRes: corev1api.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "svc-1",
@@ -210,6 +218,7 @@ func TestServiceActionExecute(t *testing.T) {
},
},
},
restore: builder.ForRestore(api.DefaultNamespace, "").Result(),
expectedRes: corev1api.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "svc-1",
@@ -246,6 +255,7 @@ func TestServiceActionExecute(t *testing.T) {
},
},
},
restore: builder.ForRestore(api.DefaultNamespace, "").Result(),
expectedRes: corev1api.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "svc-1",
@@ -266,6 +276,46 @@ func TestServiceActionExecute(t *testing.T) {
},
},
},
{
name: "If PreserveNodePorts is True in restore spec then nodePort always preserved.",
obj: corev1api.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "svc-1",
},
Spec: corev1api.ServiceSpec{
Ports: []corev1api.ServicePort{
{
Name: "http",
Port: 80,
NodePort: 8080,
},
{
Name: "hepsiburada",
NodePort: 9025,
},
},
},
},
restore: builder.ForRestore(api.DefaultNamespace, "").PreserveNodePorts(true).Result(),
expectedRes: corev1api.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "svc-1",
},
Spec: corev1api.ServiceSpec{
Ports: []corev1api.ServicePort{
{
Name: "http",
Port: 80,
NodePort: 8080,
},
{
Name: "hepsiburada",
NodePort: 9025,
},
},
},
},
},
}
for _, test := range tests {
@@ -278,7 +328,7 @@ func TestServiceActionExecute(t *testing.T) {
res, err := action.Execute(&velero.RestoreItemActionExecuteInput{
Item: &unstructured.Unstructured{Object: unstructuredSvc},
ItemFromBackup: &unstructured.Unstructured{Object: unstructuredSvc},
Restore: nil,
Restore: test.restore,
})
if assert.Equal(t, test.expectedErr, err != nil) && !test.expectedErr {
@@ -46,6 +46,8 @@ Label selector: <none>
Restore PVs: auto
Preserve Service NodePorts: auto
Phase: Completed
Validation errors: <none>
@@ -36,6 +36,56 @@ Available Commands:
logs Get restore logs
```
## What happens to NodePorts when restoring Services
**Auto assigned** NodePorts **deleted** by default and Services get new **auto assigned** nodePorts after restore.
**Explicitly specified** NodePorts auto detected using **`last-applied-config`** annotation and **preserved** after restore. NodePorts can be explicitly specified as .spec.ports[*].nodePort field on Service definition.
#### Always Preserve NodePorts
It is not always possible to set nodePorts explicitly on some big clusters because of operation complexity. Official Kubernetes documents states that preventing port collisions is responsibility of the user when explicitly specifying nodePorts:
```
If you want a specific port number, you can specify a value in the `nodePort` field. The control plane will either allocate you that port or report that the API transaction failed. This means that you need to take care of possible port collisions yourself. You also have to use a valid port number, one that's inside the range configured for NodePort use.
https://kubernetes.io/docs/concepts/services-networking/service/#nodeport
```
The clusters which are not explicitly specifying nodePorts still may need to restore original NodePorts in case of disaster. Auto assigned nodePorts most probably defined on Load Balancers which located front side of cluster. Changing all these nodePorts on Load Balancers is another operation complexity after disaster if nodePorts are changed.
Velero has a flag to let user deciding the preservation of nodePorts. **`velero restore create`** sub command has **`--preserve-nodeports`** flag to **preserve** Service nodePorts **always** regardless of nodePorts **explicitly specified** or **not**. This flag used for preserving the original nodePorts from backup and can be used as **`--preserve-nodeports`** or **`--preserve-nodeports=true`**
If this flag given and/or set to true, Velero does not remove the nodePorts when restoring Service and tries to use the nodePorts which written on backup.
Trying to preserve nodePorts may cause **port conflicts** when restoring on situations below:
- If the nodePort from the backup already allocated on the target cluster then Velero prints error log as shown below and continue to restore operation.
```
time="2020-11-23T12:58:31+03:00" level=info msg="Executing item action for services" logSource="pkg/restore/restore.go:1002" restore=velero/test-with-3-svc-20201123125825
time="2020-11-23T12:58:31+03:00" level=info msg="Restoring Services with original NodePort(s)" cmd=_output/bin/linux/amd64/velero logSource="pkg/restore/service_action.go:61" pluginName=velero restore=velero/test-with-3-svc-20201123125825
time="2020-11-23T12:58:31+03:00" level=info msg="Attempting to restore Service: hello-service" logSource="pkg/restore/restore.go:1107" restore=velero/test-with-3-svc-20201123125825
time="2020-11-23T12:58:31+03:00" level=error msg="error restoring hello-service: Service \"hello-service\" is invalid: spec.ports[0].nodePort: Invalid value: 31536: provided port is already allocated" logSource="pkg/restore/restore.go:1170" restore=velero/test-with-3-svc-20201123125825
```
- If the nodePort from the backup is not in the nodePort range of target cluster then Velero prints error log as below and continue to restore operation. Kubernetes default nodePort range is 30000-32767 but on the example cluster nodePort range is 20000-22767 and tried to restore Service with nodePort 31536
```
time="2020-11-23T13:09:17+03:00" level=info msg="Executing item action for services" logSource="pkg/restore/restore.go:1002" restore=velero/test-with-3-svc-20201123130915
time="2020-11-23T13:09:17+03:00" level=info msg="Restoring Services with original NodePort(s)" cmd=_output/bin/linux/amd64/velero logSource="pkg/restore/service_action.go:61" pluginName=velero restore=velero/test-with-3-svc-20201123130915
time="2020-11-23T13:09:17+03:00" level=info msg="Attempting to restore Service: hello-service" logSource="pkg/restore/restore.go:1107" restore=velero/test-with-3-svc-20201123130915
time="2020-11-23T13:09:17+03:00" level=error msg="error restoring hello-service: Service \"hello-service\" is invalid: spec.ports[0].nodePort: Invalid value: 31536: provided port is not in the valid range. The range of valid ports is 20000-22767" logSource="pkg/restore/restore.go:1170" restore=velero/test-with-3-svc-20201123130915
```
## Changing PV/PVC Storage Classes
Velero can change the storage class of persistent volumes and persistent volume claims during restores. To configure a storage class mapping, create a config map in the Velero namespace like the following:
@@ -46,6 +46,8 @@ Label selector: <none>
Restore PVs: auto
Preserve Service NodePorts: auto
Phase: Completed
Validation errors: <none>
@@ -36,6 +36,56 @@ Available Commands:
logs Get restore logs
```
## What happens to NodePorts when restoring Services
**Auto assigned** NodePorts **deleted** by default and Services get new **auto assigned** nodePorts after restore.
**Explicitly specified** NodePorts auto detected using **`last-applied-config`** annotation and **preserved** after restore. NodePorts can be explicitly specified as .spec.ports[*].nodePort field on Service definition.
#### Always Preserve NodePorts
It is not always possible to set nodePorts explicitly on some big clusters because of operation complexity. Official Kubernetes documents states that preventing port collisions is responsibility of the user when explicitly specifying nodePorts:
```
If you want a specific port number, you can specify a value in the `nodePort` field. The control plane will either allocate you that port or report that the API transaction failed. This means that you need to take care of possible port collisions yourself. You also have to use a valid port number, one that's inside the range configured for NodePort use.
https://kubernetes.io/docs/concepts/services-networking/service/#nodeport
```
The clusters which are not explicitly specifying nodePorts still may need to restore original NodePorts in case of disaster. Auto assigned nodePorts most probably defined on Load Balancers which located front side of cluster. Changing all these nodePorts on Load Balancers is another operation complexity after disaster if nodePorts are changed.
Velero has a flag to let user deciding the preservation of nodePorts. **`velero restore create`** sub command has **`--preserve-nodeports`** flag to **preserve** Service nodePorts **always** regardless of nodePorts **explicitly specified** or **not**. This flag used for preserving the original nodePorts from backup and can be used as **`--preserve-nodeports`** or **`--preserve-nodeports=true`**
If this flag given and/or set to true, Velero does not remove the nodePorts when restoring Service and tries to use the nodePorts which written on backup.
Trying to preserve nodePorts may cause **port conflicts** when restoring on situations below:
- If the nodePort from the backup already allocated on the target cluster then Velero prints error log as shown below and continue to restore operation.
```
time="2020-11-23T12:58:31+03:00" level=info msg="Executing item action for services" logSource="pkg/restore/restore.go:1002" restore=velero/test-with-3-svc-20201123125825
time="2020-11-23T12:58:31+03:00" level=info msg="Restoring Services with original NodePort(s)" cmd=_output/bin/linux/amd64/velero logSource="pkg/restore/service_action.go:61" pluginName=velero restore=velero/test-with-3-svc-20201123125825
time="2020-11-23T12:58:31+03:00" level=info msg="Attempting to restore Service: hello-service" logSource="pkg/restore/restore.go:1107" restore=velero/test-with-3-svc-20201123125825
time="2020-11-23T12:58:31+03:00" level=error msg="error restoring hello-service: Service \"hello-service\" is invalid: spec.ports[0].nodePort: Invalid value: 31536: provided port is already allocated" logSource="pkg/restore/restore.go:1170" restore=velero/test-with-3-svc-20201123125825
```
- If the nodePort from the backup is not in the nodePort range of target cluster then Velero prints error log as below and continue to restore operation. Kubernetes default nodePort range is 30000-32767 but on the example cluster nodePort range is 20000-22767 and tried to restore Service with nodePort 31536
```
time="2020-11-23T13:09:17+03:00" level=info msg="Executing item action for services" logSource="pkg/restore/restore.go:1002" restore=velero/test-with-3-svc-20201123130915
time="2020-11-23T13:09:17+03:00" level=info msg="Restoring Services with original NodePort(s)" cmd=_output/bin/linux/amd64/velero logSource="pkg/restore/service_action.go:61" pluginName=velero restore=velero/test-with-3-svc-20201123130915
time="2020-11-23T13:09:17+03:00" level=info msg="Attempting to restore Service: hello-service" logSource="pkg/restore/restore.go:1107" restore=velero/test-with-3-svc-20201123130915
time="2020-11-23T13:09:17+03:00" level=error msg="error restoring hello-service: Service \"hello-service\" is invalid: spec.ports[0].nodePort: Invalid value: 31536: provided port is not in the valid range. The range of valid ports is 20000-22767" logSource="pkg/restore/restore.go:1170" restore=velero/test-with-3-svc-20201123130915
```
## Changing PV/PVC Storage Classes
Velero can change the storage class of persistent volumes and persistent volume claims during restores. To configure a storage class mapping, create a config map in the Velero namespace like the following: