diff --git a/api/v1alpha1/recipe_types.go b/api/v1alpha1/recipe_types.go index dbe8bd2..3611046 100644 --- a/api/v1alpha1/recipe_types.go +++ b/api/v1alpha1/recipe_types.go @@ -10,6 +10,14 @@ import ( // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. +const ( + BackupWorkflowName string = "backup" + RestoreWorkflowName string = "restore" + + // TODO + // Do we want to add capture and recover workflows? +) + // RecipeSpec defines the desired state of Recipe type RecipeSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster @@ -30,12 +38,9 @@ type RecipeSpec struct { //+listType=map //+listMapKey=name Hooks []*Hook `json:"hooks,omitempty"` - // The sequence of actions to capture data to protect from disaster + // Workflow is the sequence of actions to take //+optional - CaptureWorkflow *Workflow `json:"captureWorkflow"` - // The sequence of actions to recover data protected from disaster - //+optional - RecoverWorkflow *Workflow `json:"recoverWorkflow"` + Workflows []*Workflow `json:"workflows"` } // Groups defined in the recipe refine / narrow-down the scope of its parent groups defined in the @@ -70,15 +75,34 @@ type Group struct { // Whether to include any cluster-scoped resources. If nil or true, cluster-scoped resources are // included if they are associated with the included namespace-scoped resources IncludeClusterResources *bool `json:"includeClusterResources,omitempty"` + // Selects namespaces by label + IncludedNamespacesByLabel *metav1.LabelSelector `json:"includedNamespacesByLabel,omitempty"` // List of namespaces to include. //+optional IncludedNamespaces []string `json:"includedNamespaces,omitempty"` + // List of namespace to exclude + ExcludedNamespaces []string `json:"excludedNamespaces,omitempty"` + // RestoreStatus restores status if set to all the includedResources specified. Specify '*' to restore all statuses for all the CRs + RestoreStatus *GroupRestoreStatus `json:"restoreStatus,omitempty"` // Defaults to true, if set to false, a failure is not necessarily handled as fatal Essential *bool `json:"essential,omitempty"` + // Whether to overwrite resources during restore. Default to false. + RestoreOverwriteResources *bool `json:"restoreOverwriteResources,omitempty"` +} + +// GroupRestoreStatus is within resource groups which instructs velero to restore status for specified resources types, * would mean all +type GroupRestoreStatus struct { + // List of resource types to include. If unspecified, all resource types are included. + IncludedResources []string `json:"includedResources,omitempty"` + // List of resource types to exclude. + ExcludedResources []string `json:"excludedResources,omitempty"` } // Workflow is the sequence of actions to take type Workflow struct { + // Name of recipe. Names "backup" and "restore" are reserved and implicitly used by default for + // backup or restore respectively + Name string `json:"name"` // List of the names of groups or hooks, in the order in which they should be executed // Format: : [/] Sequence []map[string]string `json:"sequence"` @@ -93,13 +117,11 @@ type Hook struct { // Hook name, unique within the Recipe CR Name string `json:"name"` // Namespace - //+optional - Namespace string `json:"namespace,omitempty"` + Namespace string `json:"namespace"` // Hook type // +kubebuilder:validation:Enum=exec;scale;check Type string `json:"type"` // Resource type to that a hook applies to - // +kubebuilder:validation:Enum=pod;deployment;statefulset SelectResource string `json:"selectResource,omitempty"` // If specified, resource object needs to match this label selector //+optional @@ -113,10 +135,8 @@ type Hook struct { // +kubebuilder:validation:Enum=fail;continue // +kubebuilder:default=fail OnError string `json:"onError,omitempty"` - // Default timeout applied to custom and built-in operations. If not specified, equals to 30s. - //+kubebuilder:validation:Format=duration - //+optional - Timeout *metav1.Duration `json:"timeout,omitempty"` + // Default timeout in seconds applied to custom and built-in operations. If not specified, equals to 30s. + Timeout int `json:"timeout,omitempty"` // Set of operations that the hook can be invoked for //+listType=map //+listMapKey=name @@ -136,14 +156,12 @@ type Operation struct { // The container where the command should be executed Container string `json:"container,omitempty"` // The command to execute - //+kubebuilder:validation:MinItems=1 - Command []string `json:"command"` + //+kubebuilder:validation:MinLength=1 + Command string `json:"command"` // How to handle command returning with non-zero exit code. Defaults to Fail. OnError string `json:"onError,omitempty"` - // How long to wait for the command to execute - //+kubebuilder:validation:Format=duration - //+optional - Timeout *metav1.Duration `json:"timeout,omitempty"` + // How long to wait for the command to execute, in seconds + Timeout int `json:"timeout,omitempty"` // Name of another operation that reverts the effect of this operation (e.g. quiesce vs. unquiesce) InverseOp string `json:"inverseOp,omitempty"` } @@ -156,10 +174,8 @@ type Check struct { Condition string `json:"condition,omitempty"` // How to handle when check does not become true. Defaults to Fail. OnError string `json:"onError,omitempty"` - // How long to wait for the check to execute - //+kubebuilder:validation:Format=duration - //+optional - Timeout *metav1.Duration `json:"timeout,omitempty"` + // How long to wait for the check to execute, in seconds + Timeout int `json:"timeout,omitempty"` } // RecipeStatus defines the observed state of Recipe diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 5bcf406..29acfb5 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -28,11 +28,6 @@ import ( // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Check) DeepCopyInto(out *Check) { *out = *in - if in.Timeout != nil { - in, out := &in.Timeout, &out.Timeout - *out = new(v1.Duration) - **out = **in - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Check. @@ -68,16 +63,36 @@ func (in *Group) DeepCopyInto(out *Group) { *out = new(bool) **out = **in } + if in.IncludedNamespacesByLabel != nil { + in, out := &in.IncludedNamespacesByLabel, &out.IncludedNamespacesByLabel + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } if in.IncludedNamespaces != nil { in, out := &in.IncludedNamespaces, &out.IncludedNamespaces *out = make([]string, len(*in)) copy(*out, *in) } + if in.ExcludedNamespaces != nil { + in, out := &in.ExcludedNamespaces, &out.ExcludedNamespaces + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.RestoreStatus != nil { + in, out := &in.RestoreStatus, &out.RestoreStatus + *out = new(GroupRestoreStatus) + (*in).DeepCopyInto(*out) + } if in.Essential != nil { in, out := &in.Essential, &out.Essential *out = new(bool) **out = **in } + if in.RestoreOverwriteResources != nil { + in, out := &in.RestoreOverwriteResources, &out.RestoreOverwriteResources + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Group. @@ -90,6 +105,31 @@ func (in *Group) DeepCopy() *Group { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GroupRestoreStatus) DeepCopyInto(out *GroupRestoreStatus) { + *out = *in + if in.IncludedResources != nil { + in, out := &in.IncludedResources, &out.IncludedResources + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ExcludedResources != nil { + in, out := &in.ExcludedResources, &out.ExcludedResources + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroupRestoreStatus. +func (in *GroupRestoreStatus) DeepCopy() *GroupRestoreStatus { + if in == nil { + return nil + } + out := new(GroupRestoreStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Hook) DeepCopyInto(out *Hook) { *out = *in @@ -98,11 +138,6 @@ func (in *Hook) DeepCopyInto(out *Hook) { *out = new(v1.LabelSelector) (*in).DeepCopyInto(*out) } - if in.Timeout != nil { - in, out := &in.Timeout, &out.Timeout - *out = new(v1.Duration) - **out = **in - } if in.Ops != nil { in, out := &in.Ops, &out.Ops *out = make([]*Operation, len(*in)) @@ -110,7 +145,7 @@ func (in *Hook) DeepCopyInto(out *Hook) { if (*in)[i] != nil { in, out := &(*in)[i], &(*out)[i] *out = new(Operation) - (*in).DeepCopyInto(*out) + **out = **in } } } @@ -121,7 +156,7 @@ func (in *Hook) DeepCopyInto(out *Hook) { if (*in)[i] != nil { in, out := &(*in)[i], &(*out)[i] *out = new(Check) - (*in).DeepCopyInto(*out) + **out = **in } } } @@ -145,16 +180,6 @@ func (in *Hook) DeepCopy() *Hook { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Operation) DeepCopyInto(out *Operation) { *out = *in - if in.Command != nil { - in, out := &in.Command, &out.Command - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.Timeout != nil { - in, out := &in.Timeout, &out.Timeout - *out = new(v1.Duration) - **out = **in - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Operation. @@ -256,15 +281,16 @@ func (in *RecipeSpec) DeepCopyInto(out *RecipeSpec) { } } } - if in.CaptureWorkflow != nil { - in, out := &in.CaptureWorkflow, &out.CaptureWorkflow - *out = new(Workflow) - (*in).DeepCopyInto(*out) - } - if in.RecoverWorkflow != nil { - in, out := &in.RecoverWorkflow, &out.RecoverWorkflow - *out = new(Workflow) - (*in).DeepCopyInto(*out) + if in.Workflows != nil { + in, out := &in.Workflows, &out.Workflows + *out = make([]*Workflow, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Workflow) + (*in).DeepCopyInto(*out) + } + } } } diff --git a/config/crd/bases/ramendr.openshift.io_recipes.yaml b/config/crd/bases/ramendr.openshift.io_recipes.yaml index 4508215..8629d8e 100644 --- a/config/crd/bases/ramendr.openshift.io_recipes.yaml +++ b/config/crd/bases/ramendr.openshift.io_recipes.yaml @@ -44,31 +44,6 @@ spec: Type of application the recipe is designed for. (AppType is not used yet. For now, we will match the name of the app CR) type: string - captureWorkflow: - description: The sequence of actions to capture data to protect from - disaster - properties: - failOn: - default: any-error - description: 'Implies behaviour in case of failure: any-error - (default), essential-error, full-error' - enum: - - any-error - - essential-error - - full-error - type: string - sequence: - description: |- - List of the names of groups or hooks, in the order in which they should be executed - Format: : [/] - items: - additionalProperties: - type: string - type: object - type: array - required: - - sequence - type: object groups: description: List of one or multiple groups items: @@ -86,6 +61,11 @@ spec: description: Defaults to true, if set to false, a failure is not necessarily handled as fatal type: boolean + excludedNamespaces: + description: List of namespace to exclude + items: + type: string + type: array excludedResourceTypes: description: List of resource types to exclude items: @@ -101,6 +81,50 @@ spec: items: type: string type: array + includedNamespacesByLabel: + description: Selects namespaces by label + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic includedResourceTypes: description: List of resource types to include. If unspecified, all resource types are included. @@ -164,6 +188,27 @@ spec: parent group is represented by the implicit default group of Application CR (implies the Application CR does not specify groups explicitly). type: string + restoreOverwriteResources: + description: Whether to overwrite resources during restore. + Default to false. + type: boolean + restoreStatus: + description: RestoreStatus restores status if set to all the + includedResources specified. Specify '*' to restore all statuses + for all the CRs + properties: + excludedResources: + description: List of resource types to exclude. + items: + type: string + type: array + includedResources: + description: List of resource types to include. If unspecified, + all resource types are included. + items: + type: string + type: array + type: object selectResource: description: Determines the resource type which the fields labelSelector and nameSelector apply to for selecting PVCs. Default selection @@ -211,9 +256,9 @@ spec: true. Defaults to Fail. type: string timeout: - description: How long to wait for the check to execute - format: duration - type: string + description: How long to wait for the check to execute, + in seconds + type: integer required: - name type: object @@ -296,10 +341,8 @@ spec: properties: command: description: The command to execute - items: - type: string - minItems: 1 - type: array + minLength: 1 + type: string container: description: The container where the command should be executed @@ -317,9 +360,9 @@ spec: exit code. Defaults to Fail. type: string timeout: - description: How long to wait for the command to execute - format: duration - type: string + description: How long to wait for the command to execute, + in seconds + type: integer required: - command - name @@ -330,10 +373,6 @@ spec: x-kubernetes-list-type: map selectResource: description: Resource type to that a hook applies to - enum: - - pod - - deployment - - statefulset type: string singlePodOnly: description: |- @@ -341,10 +380,9 @@ spec: match the selector type: boolean timeout: - description: Default timeout applied to custom and built-in - operations. If not specified, equals to 30s. - format: duration - type: string + description: Default timeout in seconds applied to custom and + built-in operations. If not specified, equals to 30s. + type: integer type: description: Hook type enum: @@ -354,37 +392,13 @@ spec: type: string required: - name + - namespace - type type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map - recoverWorkflow: - description: The sequence of actions to recover data protected from - disaster - properties: - failOn: - default: any-error - description: 'Implies behaviour in case of failure: any-error - (default), essential-error, full-error' - enum: - - any-error - - essential-error - - full-error - type: string - sequence: - description: |- - List of the names of groups or hooks, in the order in which they should be executed - Format: : [/] - items: - additionalProperties: - type: string - type: object - type: array - required: - - sequence - type: object volumes: description: Volumes to protect from disaster properties: @@ -397,6 +411,11 @@ spec: description: Defaults to true, if set to false, a failure is not necessarily handled as fatal type: boolean + excludedNamespaces: + description: List of namespace to exclude + items: + type: string + type: array excludedResourceTypes: description: List of resource types to exclude items: @@ -412,6 +431,50 @@ spec: items: type: string type: array + includedNamespacesByLabel: + description: Selects namespaces by label + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic includedResourceTypes: description: List of resource types to include. If unspecified, all resource types are included. @@ -475,6 +538,26 @@ spec: parent group is represented by the implicit default group of Application CR (implies the Application CR does not specify groups explicitly). type: string + restoreOverwriteResources: + description: Whether to overwrite resources during restore. Default + to false. + type: boolean + restoreStatus: + description: RestoreStatus restores status if set to all the includedResources + specified. Specify '*' to restore all statuses for all the CRs + properties: + excludedResources: + description: List of resource types to exclude. + items: + type: string + type: array + includedResources: + description: List of resource types to include. If unspecified, + all resource types are included. + items: + type: string + type: array + type: object selectResource: description: Determines the resource type which the fields labelSelector and nameSelector apply to for selecting PVCs. Default selection @@ -496,6 +579,39 @@ spec: - name - type type: object + workflows: + description: Workflow is the sequence of actions to take + items: + description: Workflow is the sequence of actions to take + properties: + failOn: + default: any-error + description: 'Implies behaviour in case of failure: any-error + (default), essential-error, full-error' + enum: + - any-error + - essential-error + - full-error + type: string + name: + description: |- + Name of recipe. Names "backup" and "restore" are reserved and implicitly used by default for + backup or restore respectively + type: string + sequence: + description: |- + List of the names of groups or hooks, in the order in which they should be executed + Format: : [/] + items: + additionalProperties: + type: string + type: object + type: array + required: + - name + - sequence + type: object + type: array required: - appType type: object diff --git a/controllers/recipe_controller_test.go b/controllers/recipe_controller_test.go index df0f9bc..fa620d2 100644 --- a/controllers/recipe_controller_test.go +++ b/controllers/recipe_controller_test.go @@ -133,20 +133,19 @@ var _ = Describe("RecipeController", func() { Expect(err).ToNot(BeNil()) }) - echoCommand := func(s string) []string { - return []string{"/bin/sh", "-c", "echo", s} - } - emptyCommand := func(string) []string { - return []string{} + + echoCommandTemplate := "/bin/sh/ -c echo" + emptyCommandTemplate := "" + + commandGenerator := func(commandTemplate, suffix string) string { + return commandTemplate + suffix } - hookOps := func(command func(string) []string, opNames ...string) []*Recipe.Operation { - ops := make([]*Recipe.Operation, len(opNames)) - for i, opName := range opNames { - ops[i] = &Recipe.Operation{Name: opName, Command: command(opName)} - } - return ops + appendOp := func(ops []*Recipe.Operation, opName, commandTemplate, suffix string) []*Recipe.Operation { + op := &Recipe.Operation{Name: opName, Command: commandGenerator(commandTemplate, suffix)} + return append(ops, op) } + hookOpsRecipe := func(ops []*Recipe.Operation) *Recipe.Recipe { return &Recipe.Recipe{ TypeMeta: metav1.TypeMeta{Kind: "Recipe", APIVersion: "ramendr.openshift.io/v1alpha1"}, @@ -164,17 +163,21 @@ var _ = Describe("RecipeController", func() { } } It("error on empty command", func() { - recipe := hookOpsRecipe(hookOps(emptyCommand, "op-1")) + ops := []*Recipe.Operation{} + + ops = appendOp(ops, "op-1", emptyCommandTemplate, "") + + recipe := hookOpsRecipe(ops) Expect(k8sClient.Create(context.TODO(), recipe)).To(MatchError(func() *errors.StatusError { path := field.NewPath("spec", "hooks[0]", "ops[0]", "command") - value := 0 + value := "" return errors.NewInvalid( schema.GroupKind{Group: Recipe.GroupVersion.Group, Kind: "Recipe"}, recipe.Name, field.ErrorList{ field.Invalid( - path, value, validationErrors.TooFewItems( + path, value, validationErrors.TooShort( path.String(), "body", 1, @@ -186,12 +189,22 @@ var _ = Describe("RecipeController", func() { }())) }) It("allow unique Ops names", func() { - err := k8sClient.Create(context.TODO(), hookOpsRecipe(hookOps(echoCommand, "op-1", "op-2"))) + ops := []*Recipe.Operation{} + + ops = appendOp(ops, "op-1", echoCommandTemplate, "aaa") + ops = appendOp(ops, "op-2", echoCommandTemplate, "bbb") + + err := k8sClient.Create(context.TODO(), hookOpsRecipe(ops)) Expect(err).To(BeNil()) }) It("error on duplicate Ops names", func() { - err := k8sClient.Create(context.TODO(), hookOpsRecipe(hookOps(echoCommand, "op-1", "op-1"))) + ops := []*Recipe.Operation{} + + ops = appendOp(ops, "op-1", echoCommandTemplate, "aaa") + ops = appendOp(ops, "op-1", echoCommandTemplate, "bbb") + + err := k8sClient.Create(context.TODO(), hookOpsRecipe(ops)) Expect(err).ToNot(BeNil()) })