diff --git a/docs/cli-reference/ark.md b/docs/cli-reference/ark.md index e09a97148..b66760527 100644 --- a/docs/cli-reference/ark.md +++ b/docs/cli-reference/ark.md @@ -38,7 +38,7 @@ operations can also be performed as 'ark backup get' and 'ark schedule create'. * [ark describe](ark_describe.md) - Describe ark resources * [ark get](ark_get.md) - Get ark resources * [ark plugin](ark_plugin.md) - Work with plugins -* [ark restic](ark_restic.md) - Work with restic repositories +* [ark restic](ark_restic.md) - Work with restic * [ark restore](ark_restore.md) - Work with restores * [ark schedule](ark_schedule.md) - Work with schedules * [ark server](ark_server.md) - Run the ark server diff --git a/docs/cli-reference/ark_restic.md b/docs/cli-reference/ark_restic.md index 16bbad33c..27ba211f1 100644 --- a/docs/cli-reference/ark_restic.md +++ b/docs/cli-reference/ark_restic.md @@ -1,11 +1,11 @@ ## ark restic -Work with restic repositories +Work with restic ### Synopsis -Work with restic repositories +Work with restic ### Options @@ -30,6 +30,6 @@ Work with restic repositories ### SEE ALSO * [ark](ark.md) - Back up and restore Kubernetes cluster resources. -* [ark restic init-repository](ark_restic_init-repository.md) - create an encryption key for a restic repository +* [ark restic repo](ark_restic_repo.md) - Work with restic repositories * [ark restic server](ark_restic_server.md) - Run the ark restic server diff --git a/docs/cli-reference/ark_restic_repo.md b/docs/cli-reference/ark_restic_repo.md new file mode 100644 index 000000000..43285031b --- /dev/null +++ b/docs/cli-reference/ark_restic_repo.md @@ -0,0 +1,35 @@ +## ark restic repo + +Work with restic repositories + +### Synopsis + + +Work with restic repositories + +### Options + +``` + -h, --help help for repo +``` + +### Options inherited from parent commands + +``` + --alsologtostderr log to standard error as well as files + --kubeconfig string Path to the kubeconfig file to use to talk to the Kubernetes apiserver. If unset, try the environment variable KUBECONFIG, as well as in-cluster configuration + --kubecontext string The context to use to talk to the Kubernetes apiserver. If unset defaults to whatever your current-context is (kubectl config current-context) + --log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0) + --log_dir string If non-empty, write log files in this directory + --logtostderr log to standard error instead of files + -n, --namespace string The namespace in which Ark should operate (default "heptio-ark") + --stderrthreshold severity logs at or above this threshold go to stderr (default 2) + -v, --v Level log level for V logs + --vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging +``` + +### SEE ALSO +* [ark restic](ark_restic.md) - Work with restic +* [ark restic repo get](ark_restic_repo_get.md) - Get restic repositories +* [ark restic repo init](ark_restic_repo_init.md) - initialize a restic repository for a specified namespace + diff --git a/docs/cli-reference/ark_restic_init-repository.md b/docs/cli-reference/ark_restic_repo_get.md similarity index 64% rename from docs/cli-reference/ark_restic_init-repository.md rename to docs/cli-reference/ark_restic_repo_get.md index 2704ea0e2..de9fa38ea 100644 --- a/docs/cli-reference/ark_restic_init-repository.md +++ b/docs/cli-reference/ark_restic_repo_get.md @@ -1,23 +1,24 @@ -## ark restic init-repository +## ark restic repo get -create an encryption key for a restic repository +Get restic repositories ### Synopsis -create an encryption key for a restic repository +Get restic repositories ``` -ark restic init-repository [flags] +ark restic repo get [flags] ``` ### Options ``` - -h, --help help for init-repository - --key-data string Encryption key for the restic repository. Optional; if unset, Ark will generate a random key for you. - --key-file string Path to file containing the encryption key for the restic repository. Optional; if unset, Ark will generate a random key for you. - --key-size int Size of the generated key for the restic repository (default 1024) + -h, --help help for get + --label-columns stringArray a comma-separated list of labels to be displayed as columns + -o, --output string Output display format. For create commands, display the object but do not send it to the server. Valid formats are 'table', 'json', and 'yaml'. (default "table") + -l, --selector string only show items matching this label selector + --show-labels show labels in the last column ``` ### Options inherited from parent commands @@ -36,5 +37,5 @@ ark restic init-repository [flags] ``` ### SEE ALSO -* [ark restic](ark_restic.md) - Work with restic repositories +* [ark restic repo](ark_restic_repo.md) - Work with restic repositories diff --git a/docs/cli-reference/ark_restic_repo_init.md b/docs/cli-reference/ark_restic_repo_init.md new file mode 100644 index 000000000..c0c171608 --- /dev/null +++ b/docs/cli-reference/ark_restic_repo_init.md @@ -0,0 +1,41 @@ +## ark restic repo init + +initialize a restic repository for a specified namespace + +### Synopsis + + +initialize a restic repository for a specified namespace + +``` +ark restic repo init NAMESPACE [flags] +``` + +### Options + +``` + -h, --help help for init + --key-data string Encryption key for the restic repository. Optional; if unset, Ark will generate a random key for you. + --key-file string Path to file containing the encryption key for the restic repository. Optional; if unset, Ark will generate a random key for you. + --key-size int Size of the generated key for the restic repository (default 1024) + --maintenance-frequency duration How often maintenance (i.e. restic prune & check) is run on the repository (default 24h0m0s) +``` + +### Options inherited from parent commands + +``` + --alsologtostderr log to standard error as well as files + --kubeconfig string Path to the kubeconfig file to use to talk to the Kubernetes apiserver. If unset, try the environment variable KUBECONFIG, as well as in-cluster configuration + --kubecontext string The context to use to talk to the Kubernetes apiserver. If unset defaults to whatever your current-context is (kubectl config current-context) + --log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0) + --log_dir string If non-empty, write log files in this directory + --logtostderr log to standard error instead of files + -n, --namespace string The namespace in which Ark should operate (default "heptio-ark") + --stderrthreshold severity logs at or above this threshold go to stderr (default 2) + -v, --v Level log level for V logs + --vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging +``` + +### SEE ALSO +* [ark restic repo](ark_restic_repo.md) - Work with restic repositories + diff --git a/docs/cli-reference/ark_restic_server.md b/docs/cli-reference/ark_restic_server.md index eaca3b98d..02e9df772 100644 --- a/docs/cli-reference/ark_restic_server.md +++ b/docs/cli-reference/ark_restic_server.md @@ -34,5 +34,5 @@ ark restic server [flags] ``` ### SEE ALSO -* [ark restic](ark_restic.md) - Work with restic repositories +* [ark restic](ark_restic.md) - Work with restic diff --git a/examples/aws/10-deployment.yaml b/examples/aws/10-deployment.yaml index e824c24c9..1742c4713 100644 --- a/examples/aws/10-deployment.yaml +++ b/examples/aws/10-deployment.yaml @@ -43,12 +43,18 @@ spec: mountPath: /credentials - name: plugins mountPath: /plugins + - name: scratch + mountPath: /scratch env: - name: AWS_SHARED_CREDENTIALS_FILE value: /credentials/cloud + - name: ARK_SCRATCH_DIR + value: /scratch volumes: - name: cloud-credentials secret: secretName: cloud-credentials - name: plugins emptyDir: {} + - name: scratch + emptyDir: {} \ No newline at end of file diff --git a/examples/aws/20-restic-daemonset.yaml b/examples/aws/20-restic-daemonset.yaml index d180a1bdb..01ff91b74 100644 --- a/examples/aws/20-restic-daemonset.yaml +++ b/examples/aws/20-restic-daemonset.yaml @@ -36,6 +36,8 @@ spec: - name: host-pods hostPath: path: /var/lib/kubelet/pods + - name: scratch + emptyDir: {} containers: - name: ark image: gcr.io/heptio-images/ark:latest @@ -49,6 +51,8 @@ spec: mountPath: /credentials - name: host-pods mountPath: /host_pods + - name: scratch + mountPath: /scratch env: - name: NODE_NAME valueFrom: @@ -59,4 +63,6 @@ spec: fieldRef: fieldPath: metadata.namespace - name: AWS_SHARED_CREDENTIALS_FILE - value: /credentials/cloud \ No newline at end of file + value: /credentials/cloud + - name: ARK_SCRATCH_DIR + value: /scratch \ No newline at end of file diff --git a/examples/azure/00-ark-deployment.yaml b/examples/azure/00-ark-deployment.yaml index 614f0a37a..93f1944a8 100644 --- a/examples/azure/00-ark-deployment.yaml +++ b/examples/azure/00-ark-deployment.yaml @@ -44,11 +44,18 @@ spec: envFrom: - secretRef: name: cloud-credentials + env: + - name: ARK_SCRATCH_DIR + value: /scratch volumeMounts: - name: plugins mountPath: /plugins + - name: scratch + mountPath: /scratch volumes: - name: plugins emptyDir: {} + - name: scratch + emptyDir: {} nodeSelector: beta.kubernetes.io/os: linux diff --git a/examples/azure/20-restic-daemonset.yaml b/examples/azure/20-restic-daemonset.yaml index 885242a91..356d33a8f 100644 --- a/examples/azure/20-restic-daemonset.yaml +++ b/examples/azure/20-restic-daemonset.yaml @@ -33,6 +33,8 @@ spec: - name: host-pods hostPath: path: /var/lib/kubelet/pods + - name: scratch + emptyDir: {} containers: - name: ark image: gcr.io/heptio-images/ark:latest @@ -44,6 +46,8 @@ spec: volumeMounts: - name: host-pods mountPath: /host_pods + - name: scratch + mountPath: /scratch envFrom: - secretRef: name: cloud-credentials @@ -66,4 +70,5 @@ spec: secretKeyRef: name: cloud-credentials key: AZURE_STORAGE_KEY - \ No newline at end of file + - name: ARK_SCRATCH_DIR + value: /scratch \ No newline at end of file diff --git a/examples/common/00-prereqs.yaml b/examples/common/00-prereqs.yaml index c7b5c4b45..53660427e 100644 --- a/examples/common/00-prereqs.yaml +++ b/examples/common/00-prereqs.yaml @@ -132,6 +132,21 @@ spec: plural: podvolumerestores kind: PodVolumeRestore +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: resticrepositories.ark.heptio.com + labels: + component: ark +spec: + group: ark.heptio.com + version: v1 + scope: Namespaced + names: + plural: resticrepositories + kind: ResticRepository + --- apiVersion: v1 kind: Namespace diff --git a/examples/gcp/10-deployment.yaml b/examples/gcp/10-deployment.yaml index 312bfe82c..98db63486 100644 --- a/examples/gcp/10-deployment.yaml +++ b/examples/gcp/10-deployment.yaml @@ -46,12 +46,18 @@ spec: mountPath: /credentials - name: plugins mountPath: /plugins + - name: scratch + mountPath: /scratch env: - name: GOOGLE_APPLICATION_CREDENTIALS value: /credentials/cloud + - name: ARK_SCRATCH_DIR + value: /scratch volumes: - name: cloud-credentials secret: secretName: cloud-credentials - name: plugins emptyDir: {} + - name: scratch + emptyDir: {} diff --git a/examples/gcp/20-restic-daemonset.yaml b/examples/gcp/20-restic-daemonset.yaml index c88e803b8..8c63c9094 100644 --- a/examples/gcp/20-restic-daemonset.yaml +++ b/examples/gcp/20-restic-daemonset.yaml @@ -36,6 +36,8 @@ spec: - name: host-pods hostPath: path: /var/lib/kubelet/pods + - name: scratch + emptyDir: {} containers: - name: ark image: gcr.io/heptio-images/ark:latest @@ -49,6 +51,8 @@ spec: mountPath: /credentials - name: host-pods mountPath: /host_pods + - name: scratch + mountPath: /scratch env: - name: NODE_NAME valueFrom: @@ -59,4 +63,6 @@ spec: fieldRef: fieldPath: metadata.namespace - name: GOOGLE_APPLICATION_CREDENTIALS - value: /credentials/cloud \ No newline at end of file + value: /credentials/cloud + - name: ARK_SCRATCH_DIR + value: /scratch \ No newline at end of file diff --git a/examples/ibm/10-deployment.yaml b/examples/ibm/10-deployment.yaml index fc3895dcc..8d54af35b 100644 --- a/examples/ibm/10-deployment.yaml +++ b/examples/ibm/10-deployment.yaml @@ -46,12 +46,18 @@ spec: mountPath: /credentials - name: plugins mountPath: /plugins + - name: scratch + mountPath: /scratch env: - name: AWS_SHARED_CREDENTIALS_FILE value: /credentials/cloud + - name: ARK_SCRATCH_DIR + value: /scratch volumes: - name: cloud-credentials secret: secretName: cloud-credentials - name: plugins emptyDir: {} + - name: scratch + emptyDir: {} diff --git a/examples/minio/20-ark-deployment.yaml b/examples/minio/20-ark-deployment.yaml index 3b1984575..33a16f7e4 100644 --- a/examples/minio/20-ark-deployment.yaml +++ b/examples/minio/20-ark-deployment.yaml @@ -46,12 +46,18 @@ spec: mountPath: /credentials - name: plugins mountPath: /plugins + - name: scratch + mountPath: /scratch env: - name: AWS_SHARED_CREDENTIALS_FILE value: /credentials/cloud + - name: ARK_SCRATCH_DIR + value: /scratch volumes: - name: cloud-credentials secret: secretName: cloud-credentials - name: plugins emptyDir: {} + - name: scratch + emptyDir: {} diff --git a/examples/minio/30-restic-daemonset.yaml b/examples/minio/30-restic-daemonset.yaml index b4275890c..ca44967b5 100644 --- a/examples/minio/30-restic-daemonset.yaml +++ b/examples/minio/30-restic-daemonset.yaml @@ -36,6 +36,8 @@ spec: - name: host-pods hostPath: path: /var/lib/kubelet/pods + - name: scratch + emptyDir: {} containers: - name: ark image: gcr.io/heptio-images/ark:latest @@ -49,6 +51,8 @@ spec: mountPath: /credentials - name: host-pods mountPath: /host_pods + - name: scratch + mountPath: /scratch env: - name: NODE_NAME valueFrom: @@ -59,4 +63,6 @@ spec: fieldRef: fieldPath: metadata.namespace - name: AWS_SHARED_CREDENTIALS_FILE - value: /credentials/cloud \ No newline at end of file + value: /credentials/cloud + - name: ARK_SCRATCH_DIR + value: /scratch \ No newline at end of file diff --git a/pkg/apis/ark/v1/pod_volume_backup.go b/pkg/apis/ark/v1/pod_volume_backup.go index 37be67e98..f54a81e0f 100644 --- a/pkg/apis/ark/v1/pod_volume_backup.go +++ b/pkg/apis/ark/v1/pod_volume_backup.go @@ -33,9 +33,8 @@ type PodVolumeBackupSpec struct { // up. Volume string `json:"volume"` - // RepoPrefix is the restic repository prefix (i.e. not containing - // the repository name itself). - RepoPrefix string `json:"repoPrefix"` + // RepoIdentifier is the restic repository identifier. + RepoIdentifier string `json:"repoIdentifier"` // Tags are a map of key-value pairs that should be applied to the // volume backup as tags. diff --git a/pkg/apis/ark/v1/pod_volume_restore.go b/pkg/apis/ark/v1/pod_volume_restore.go index 3c83b5607..90033378e 100644 --- a/pkg/apis/ark/v1/pod_volume_restore.go +++ b/pkg/apis/ark/v1/pod_volume_restore.go @@ -29,9 +29,8 @@ type PodVolumeRestoreSpec struct { // Volume is the name of the volume within the Pod to be restored. Volume string `json:"volume"` - // RepoPrefix is the restic repository prefix (i.e. not containing - // the repository name itself). - RepoPrefix string `json:"repoPrefix"` + // RepoIdentifier is the restic repository identifier. + RepoIdentifier string `json:"repoIdentifier"` // SnapshotID is the ID of the volume snapshot to be restored. SnapshotID string `json:"snapshotID"` diff --git a/pkg/apis/ark/v1/register.go b/pkg/apis/ark/v1/register.go index 5bf88b63e..db92547c9 100644 --- a/pkg/apis/ark/v1/register.go +++ b/pkg/apis/ark/v1/register.go @@ -59,6 +59,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &PodVolumeBackupList{}, &PodVolumeRestore{}, &PodVolumeRestoreList{}, + &ResticRepository{}, + &ResticRepositoryList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/pkg/apis/ark/v1/restic_repository.go b/pkg/apis/ark/v1/restic_repository.go new file mode 100644 index 000000000..236cf5143 --- /dev/null +++ b/pkg/apis/ark/v1/restic_repository.go @@ -0,0 +1,72 @@ +/* +Copyright 2018 the Heptio Ark 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 v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ResticRepositorySpec is the specification for a ResticRepository. +type ResticRepositorySpec struct { + // MaintenanceFrequency is how often maintenance should be run. + MaintenanceFrequency metav1.Duration `json:"maintenanceFrequency"` + + // ResticIdentifier is the full restic-compatible string for identifying + // this repository. + ResticIdentifier string `json:"resticIdentifier"` +} + +// ResticRepositoryPhase represents the lifecycle phase of a ResticRepository. +type ResticRepositoryPhase string + +const ( + ResticRepositoryPhaseNew ResticRepositoryPhase = "New" + ResticRepositoryPhaseReady ResticRepositoryPhase = "Ready" + ResticRepositoryPhaseNotReady ResticRepositoryPhase = "NotReady" +) + +// ResticRepositoryStatus is the current status of a ResticRepository. +type ResticRepositoryStatus struct { + // Phase is the current state of the ResticRepository. + Phase ResticRepositoryPhase `json:"phase"` + + // Message is a message about the current status of the ResticRepository. + Message string `json:"message"` + + // LastMaintenanceTime is the last time maintenance was run. + LastMaintenanceTime metav1.Time `json:"lastMaintenanceTime"` +} + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +type ResticRepository struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata"` + + Spec ResticRepositorySpec `json:"spec"` + Status ResticRepositoryStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ResticRepositoryList is a list of ResticRepositories. +type ResticRepositoryList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + Items []ResticRepository `json:"items"` +} diff --git a/pkg/apis/ark/v1/zz_generated.deepcopy.go b/pkg/apis/ark/v1/zz_generated.deepcopy.go index 062b2bcc0..0a364302f 100644 --- a/pkg/apis/ark/v1/zz_generated.deepcopy.go +++ b/pkg/apis/ark/v1/zz_generated.deepcopy.go @@ -843,6 +843,101 @@ func (in *PodVolumeRestoreStatus) DeepCopy() *PodVolumeRestoreStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResticRepository) DeepCopyInto(out *ResticRepository) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResticRepository. +func (in *ResticRepository) DeepCopy() *ResticRepository { + if in == nil { + return nil + } + out := new(ResticRepository) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ResticRepository) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResticRepositoryList) DeepCopyInto(out *ResticRepositoryList) { + *out = *in + out.TypeMeta = in.TypeMeta + out.ListMeta = in.ListMeta + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ResticRepository, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResticRepositoryList. +func (in *ResticRepositoryList) DeepCopy() *ResticRepositoryList { + if in == nil { + return nil + } + out := new(ResticRepositoryList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ResticRepositoryList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResticRepositorySpec) DeepCopyInto(out *ResticRepositorySpec) { + *out = *in + out.MaintenanceFrequency = in.MaintenanceFrequency + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResticRepositorySpec. +func (in *ResticRepositorySpec) DeepCopy() *ResticRepositorySpec { + if in == nil { + return nil + } + out := new(ResticRepositorySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResticRepositoryStatus) DeepCopyInto(out *ResticRepositoryStatus) { + *out = *in + in.LastMaintenanceTime.DeepCopyInto(&out.LastMaintenanceTime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResticRepositoryStatus. +func (in *ResticRepositoryStatus) DeepCopy() *ResticRepositoryStatus { + if in == nil { + return nil + } + out := new(ResticRepositoryStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Restore) DeepCopyInto(out *Restore) { *out = *in diff --git a/pkg/cmd/cli/restic/repo/get.go b/pkg/cmd/cli/restic/repo/get.go new file mode 100644 index 000000000..844c0ace0 --- /dev/null +++ b/pkg/cmd/cli/restic/repo/get.go @@ -0,0 +1,66 @@ +/* +Copyright 2018 the Heptio Ark 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 repo + +import ( + "github.com/spf13/cobra" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + api "github.com/heptio/ark/pkg/apis/ark/v1" + "github.com/heptio/ark/pkg/client" + "github.com/heptio/ark/pkg/cmd" + "github.com/heptio/ark/pkg/cmd/util/output" +) + +func NewGetCommand(f client.Factory, use string) *cobra.Command { + var listOptions metav1.ListOptions + + c := &cobra.Command{ + Use: use, + Short: "Get restic repositories", + Run: func(c *cobra.Command, args []string) { + err := output.ValidateFlags(c) + cmd.CheckError(err) + + arkClient, err := f.Client() + cmd.CheckError(err) + + var repos *api.ResticRepositoryList + if len(args) > 0 { + repos = new(api.ResticRepositoryList) + for _, name := range args { + repo, err := arkClient.Ark().ResticRepositories(f.Namespace()).Get(name, metav1.GetOptions{}) + cmd.CheckError(err) + repos.Items = append(repos.Items, *repo) + } + } else { + repos, err = arkClient.ArkV1().ResticRepositories(f.Namespace()).List(listOptions) + cmd.CheckError(err) + } + + _, err = output.PrintWithFormat(c, repos) + cmd.CheckError(err) + }, + } + + c.Flags().StringVarP(&listOptions.LabelSelector, "selector", "l", listOptions.LabelSelector, "only show items matching this label selector") + + output.BindFlags(c.Flags()) + + return c +} diff --git a/pkg/cmd/cli/restic/init_repository.go b/pkg/cmd/cli/restic/repo/init.go similarity index 63% rename from pkg/cmd/cli/restic/init_repository.go rename to pkg/cmd/cli/restic/repo/init.go index 18fd77f82..7b3f88f80 100644 --- a/pkg/cmd/cli/restic/init_repository.go +++ b/pkg/cmd/cli/restic/repo/init.go @@ -14,31 +14,37 @@ See the License for the specific language governing permissions and limitations under the License. */ -package restic +package repo import ( "crypto/rand" + "time" - "github.com/heptio/ark/pkg/client" - "github.com/heptio/ark/pkg/cmd" - "github.com/heptio/ark/pkg/restic" - "github.com/heptio/ark/pkg/util/filesystem" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kclientset "k8s.io/client-go/kubernetes" + + "github.com/heptio/ark/pkg/apis/ark/v1" + "github.com/heptio/ark/pkg/client" + "github.com/heptio/ark/pkg/cmd" + clientset "github.com/heptio/ark/pkg/generated/clientset/versioned" + "github.com/heptio/ark/pkg/restic" + "github.com/heptio/ark/pkg/util/filesystem" ) -func NewInitRepositoryCommand(f client.Factory) *cobra.Command { +func NewInitCommand(f client.Factory) *cobra.Command { o := NewInitRepositoryOptions() c := &cobra.Command{ - Use: "init-repository", - Short: "create an encryption key for a restic repository", - Long: "create an encryption key for a restic repository", + Use: "init NAMESPACE", + Short: "initialize a restic repository for a specified namespace", + Long: "initialize a restic repository for a specified namespace", + Args: cobra.ExactArgs(1), Run: func(c *cobra.Command, args []string) { - cmd.CheckError(o.Complete(f)) + cmd.CheckError(o.Complete(f, args)) cmd.CheckError(o.Validate(f)) cmd.CheckError(o.Run(f)) }, @@ -50,20 +56,23 @@ func NewInitRepositoryCommand(f client.Factory) *cobra.Command { } type InitRepositoryOptions struct { - Namespace string - KeyFile string - KeyData string - KeySize int + Namespace string + KeyFile string + KeyData string + KeySize int + MaintenanceFrequency time.Duration fileSystem filesystem.Interface kubeClient kclientset.Interface + arkClient clientset.Interface keyBytes []byte } func NewInitRepositoryOptions() *InitRepositoryOptions { return &InitRepositoryOptions{ - KeySize: 1024, - fileSystem: filesystem.NewFileSystem(), + KeySize: 1024, + MaintenanceFrequency: restic.DefaultMaintenanceFrequency, + fileSystem: filesystem.NewFileSystem(), } } @@ -76,9 +85,10 @@ func (o *InitRepositoryOptions) BindFlags(flags *pflag.FlagSet) { flags.StringVar(&o.KeyFile, "key-file", o.KeyFile, "Path to file containing the encryption key for the restic repository. Optional; if unset, Ark will generate a random key for you.") flags.StringVar(&o.KeyData, "key-data", o.KeyData, "Encryption key for the restic repository. Optional; if unset, Ark will generate a random key for you.") flags.IntVar(&o.KeySize, "key-size", o.KeySize, "Size of the generated key for the restic repository") + flags.DurationVar(&o.MaintenanceFrequency, "maintenance-frequency", o.MaintenanceFrequency, "How often maintenance (i.e. restic prune & check) is run on the repository") } -func (o *InitRepositoryOptions) Complete(f client.Factory) error { +func (o *InitRepositoryOptions) Complete(f client.Factory, args []string) error { if o.KeyFile != "" && o.KeyData != "" { return errKeyFileAndKeyDataProvided } @@ -87,7 +97,7 @@ func (o *InitRepositoryOptions) Complete(f client.Factory) error { return errKeySizeTooSmall } - o.Namespace = f.Namespace() + o.Namespace = args[0] switch { case o.KeyFile != "": @@ -112,6 +122,10 @@ func (o *InitRepositoryOptions) Validate(f client.Factory) error { return errors.Errorf("keyBytes is required") } + if o.MaintenanceFrequency <= 0 { + return errors.Errorf("--maintenance-frequency must be greater than zero") + } + kubeClient, err := f.KubeClient() if err != nil { return err @@ -122,9 +136,30 @@ func (o *InitRepositoryOptions) Validate(f client.Factory) error { return err } + arkClient, err := f.Client() + if err != nil { + return err + } + o.arkClient = arkClient + return nil } func (o *InitRepositoryOptions) Run(f client.Factory) error { - return restic.NewRepositoryKey(o.kubeClient.CoreV1(), o.Namespace, o.keyBytes) + if err := restic.NewRepositoryKey(o.kubeClient.CoreV1(), o.Namespace, o.keyBytes); err != nil { + return err + } + + repo := &v1.ResticRepository{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: f.Namespace(), + Name: o.Namespace, + }, + Spec: v1.ResticRepositorySpec{ + MaintenanceFrequency: metav1.Duration{Duration: o.MaintenanceFrequency}, + }, + } + + _, err := o.arkClient.ArkV1().ResticRepositories(f.Namespace()).Create(repo) + return errors.Wrap(err, "error creating ResticRepository") } diff --git a/pkg/cmd/cli/restic/init_repository_test.go b/pkg/cmd/cli/restic/repo/init_test.go similarity index 63% rename from pkg/cmd/cli/restic/init_repository_test.go rename to pkg/cmd/cli/restic/repo/init_test.go index b8d3929e9..eea504964 100644 --- a/pkg/cmd/cli/restic/init_repository_test.go +++ b/pkg/cmd/cli/restic/repo/init_test.go @@ -1,4 +1,20 @@ -package restic +/* +Copyright 2018 the Heptio Ark 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 repo import ( "testing" @@ -36,7 +52,7 @@ func (f *fakeFactory) Namespace() string { func TestComplete(t *testing.T) { // no key options provided should error o := &InitRepositoryOptions{} - err := o.Complete(&fakeFactory{}) + err := o.Complete(&fakeFactory{}, []string{"ns"}) assert.EqualError(t, err, errKeySizeTooSmall.Error()) // both KeyFile and KeyData provided should error @@ -44,7 +60,7 @@ func TestComplete(t *testing.T) { KeyFile: "/foo", KeyData: "bar", } - err = o.Complete(&fakeFactory{}) + err = o.Complete(&fakeFactory{}, []string{"ns"}) assert.EqualError(t, err, errKeyFileAndKeyDataProvided.Error()) // if KeyFile is provided, its contents are used @@ -53,20 +69,20 @@ func TestComplete(t *testing.T) { KeyFile: "/foo", fileSystem: arktest.NewFakeFileSystem().WithFile("/foo", fileContents), } - assert.NoError(t, o.Complete(&fakeFactory{})) + assert.NoError(t, o.Complete(&fakeFactory{}, []string{"ns"})) assert.Equal(t, fileContents, o.keyBytes) // if KeyData is provided, it's used o = &InitRepositoryOptions{ KeyData: "bar", } - assert.NoError(t, o.Complete(&fakeFactory{})) + assert.NoError(t, o.Complete(&fakeFactory{}, []string{"ns"})) assert.Equal(t, []byte(o.KeyData), o.keyBytes) // if KeySize is provided, a random key is generated o = &InitRepositoryOptions{ KeySize: 10, } - assert.NoError(t, o.Complete(&fakeFactory{})) + assert.NoError(t, o.Complete(&fakeFactory{}, []string{"ns"})) assert.Equal(t, o.KeySize, len(o.keyBytes)) } diff --git a/pkg/cmd/cli/restic/repo/repo.go b/pkg/cmd/cli/restic/repo/repo.go new file mode 100644 index 000000000..ff7bb3b37 --- /dev/null +++ b/pkg/cmd/cli/restic/repo/repo.go @@ -0,0 +1,38 @@ +/* +Copyright 2018 the Heptio Ark 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 repo + +import ( + "github.com/spf13/cobra" + + "github.com/heptio/ark/pkg/client" +) + +func NewRepositoryCommand(f client.Factory) *cobra.Command { + c := &cobra.Command{ + Use: "repo", + Short: "Work with restic repositories", + Long: "Work with restic repositories", + } + + c.AddCommand( + NewInitCommand(f), + NewGetCommand(f, "get"), + ) + + return c +} diff --git a/pkg/cmd/cli/restic/restic.go b/pkg/cmd/cli/restic/restic.go index 3ab5f7e07..171f6e81e 100644 --- a/pkg/cmd/cli/restic/restic.go +++ b/pkg/cmd/cli/restic/restic.go @@ -20,17 +20,18 @@ import ( "github.com/spf13/cobra" "github.com/heptio/ark/pkg/client" + "github.com/heptio/ark/pkg/cmd/cli/restic/repo" ) func NewCommand(f client.Factory) *cobra.Command { c := &cobra.Command{ Use: "restic", - Short: "Work with restic repositories", - Long: "Work with restic repositories", + Short: "Work with restic", + Long: "Work with restic", } c.AddCommand( - NewInitRepositoryCommand(f), + repo.NewRepositoryCommand(f), NewServerCommand(f), ) diff --git a/pkg/cmd/server/server.go b/pkg/cmd/server/server.go index 4caa57008..28094665d 100644 --- a/pkg/cmd/server/server.go +++ b/pkg/cmd/server/server.go @@ -235,7 +235,6 @@ func (s *server) run() error { if err := s.initRestic(config.BackupStorageProvider); err != nil { return err } - s.runResticMaintenance() // warn if restic daemonset does not exist _, err := s.kubeClient.AppsV1().DaemonSets(s.namespace).Get("restic", metav1.GetOptions{}) @@ -253,20 +252,6 @@ func (s *server) run() error { return nil } -func (s *server) runResticMaintenance() { - go func() { - interval := time.Hour - - <-time.After(interval) - - wait.Forever(func() { - if err := s.resticManager.PruneAllRepos(); err != nil { - s.logger.WithError(err).Error("error pruning repos") - } - }, interval) - }() -} - func (s *server) ensureArkNamespace() error { logContext := s.logger.WithField("namespace", s.namespace) @@ -491,11 +476,11 @@ func (s *server) initRestic(config api.ObjectStorageProviderConfig) error { res, err := restic.NewRepositoryManager( s.ctx, - s.objectStore, - config, + s.namespace, s.arkClient, secretsInformer, s.kubeClient.CoreV1(), + s.sharedInformerFactory.Ark().V1().ResticRepositories(), s.logger, ) if err != nil { @@ -503,8 +488,7 @@ func (s *server) initRestic(config api.ObjectStorageProviderConfig) error { } s.resticManager = res - s.logger.Info("Checking restic repositories") - return s.resticManager.CheckAllRepos() + return nil } func (s *server) runControllers(config *api.Config) error { @@ -689,6 +673,23 @@ func (s *server) runControllers(config *api.Config) error { wg.Done() }() + if s.resticManager != nil { + resticRepoController := controller.NewResticRepositoryController( + s.logger, + s.sharedInformerFactory.Ark().V1().ResticRepositories(), + s.arkClient.ArkV1(), + config.BackupStorageProvider, + s.resticManager, + ) + wg.Add(1) + go func() { + // TODO only having a single worker may be an issue since maintenance + // can take a long time. + resticRepoController.Run(ctx, 1) + wg.Done() + }() + } + // SHARED INFORMERS HAVE TO BE STARTED AFTER ALL CONTROLLERS go s.sharedInformerFactory.Start(ctx.Done()) diff --git a/pkg/cmd/util/output/output.go b/pkg/cmd/util/output/output.go index 260bc8098..a621f5af3 100644 --- a/pkg/cmd/util/output/output.go +++ b/pkg/cmd/util/output/output.go @@ -141,6 +141,8 @@ func printTable(cmd *cobra.Command, obj runtime.Object) (bool, error) { printer.Handler(restoreColumns, nil, printRestoreList) printer.Handler(scheduleColumns, nil, printSchedule) printer.Handler(scheduleColumns, nil, printScheduleList) + printer.Handler(resticRepoColumns, nil, printResticRepo) + printer.Handler(resticRepoColumns, nil, printResticRepoList) err = printer.PrintObj(obj, os.Stdout) if err != nil { diff --git a/pkg/cmd/util/output/restic_repo_printer.go b/pkg/cmd/util/output/restic_repo_printer.go new file mode 100644 index 000000000..a2e374232 --- /dev/null +++ b/pkg/cmd/util/output/restic_repo_printer.go @@ -0,0 +1,76 @@ +/* +Copyright 2018 the Heptio Ark 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 output + +import ( + "fmt" + "io" + + "k8s.io/kubernetes/pkg/printers" + + "github.com/heptio/ark/pkg/apis/ark/v1" +) + +var ( + resticRepoColumns = []string{"NAME", "STATUS", "LAST MAINTENANCE"} +) + +func printResticRepoList(list *v1.ResticRepositoryList, w io.Writer, options printers.PrintOptions) error { + for i := range list.Items { + if err := printResticRepo(&list.Items[i], w, options); err != nil { + return err + } + } + return nil +} + +func printResticRepo(repo *v1.ResticRepository, w io.Writer, options printers.PrintOptions) error { + name := printers.FormatResourceName(options.Kind, repo.Name, options.WithKind) + + if options.WithNamespace { + if _, err := fmt.Fprintf(w, "%s\t", repo.Namespace); err != nil { + return err + } + } + + status := repo.Status.Phase + if status == "" { + status = v1.ResticRepositoryPhaseNew + } + + lastMaintenance := repo.Status.LastMaintenanceTime.String() + if repo.Status.LastMaintenanceTime.IsZero() { + lastMaintenance = "" + } + + if _, err := fmt.Fprintf( + w, + "%s\t%s\t%s", + name, + status, + lastMaintenance, + ); err != nil { + return err + } + + if _, err := fmt.Fprint(w, printers.AppendLabels(repo.Labels, options.ColumnLabels)); err != nil { + return err + } + + _, err := fmt.Fprint(w, printers.AppendAllLabels(options.ShowLabels, repo.Labels)) + return err +} diff --git a/pkg/controller/generic_controller.go b/pkg/controller/generic_controller.go index c83dd55f0..e18b3e243 100644 --- a/pkg/controller/generic_controller.go +++ b/pkg/controller/generic_controller.go @@ -151,3 +151,7 @@ func (c *genericController) enqueue(obj interface{}) { c.queue.Add(key) } + +func (c *genericController) enqueueSecond(_, obj interface{}) { + c.enqueue(obj) +} diff --git a/pkg/controller/pod_volume_backup_controller.go b/pkg/controller/pod_volume_backup_controller.go index 54dc18bfa..4a9a52005 100644 --- a/pkg/controller/pod_volume_backup_controller.go +++ b/pkg/controller/pod_volume_backup_controller.go @@ -17,12 +17,9 @@ limitations under the License. package controller import ( - "bytes" "encoding/json" "fmt" - "io/ioutil" "os" - "os/exec" "path/filepath" jsonpatch "github.com/evanphx/json-patch" @@ -40,6 +37,7 @@ import ( informers "github.com/heptio/ark/pkg/generated/informers/externalversions/ark/v1" listers "github.com/heptio/ark/pkg/generated/listers/ark/v1" "github.com/heptio/ark/pkg/restic" + arkexec "github.com/heptio/ark/pkg/util/exec" "github.com/heptio/ark/pkg/util/kube" ) @@ -175,8 +173,7 @@ func (c *podVolumeBackupController) processBackup(req *arkv1api.PodVolumeBackup) defer os.Remove(file) resticCmd := restic.BackupCommand( - req.Spec.RepoPrefix, - req.Spec.Pod.Namespace, + req.Spec.RepoIdentifier, file, path, req.Spec.Tags, @@ -184,13 +181,13 @@ func (c *podVolumeBackupController) processBackup(req *arkv1api.PodVolumeBackup) var stdout, stderr string - if stdout, stderr, err = runCommand(resticCmd.Cmd()); err != nil { + if stdout, stderr, err = arkexec.RunCommand(resticCmd.Cmd()); err != nil { log.WithError(errors.WithStack(err)).Errorf("Error running command=%s, stdout=%s, stderr=%s", resticCmd.String(), stdout, stderr) return c.fail(req, fmt.Sprintf("error running restic backup, stderr=%s: %s", stderr, err.Error()), log) } log.Debugf("Ran command=%s, stdout=%s, stderr=%s", resticCmd.String(), stdout, stderr) - snapshotID, err := restic.GetSnapshotID(req.Spec.RepoPrefix, req.Spec.Pod.Namespace, file, req.Spec.Tags) + snapshotID, err := restic.GetSnapshotID(req.Spec.RepoIdentifier, file, req.Spec.Tags) if err != nil { log.WithError(err).Error("Error getting SnapshotID") return c.fail(req, errors.Wrap(err, "error getting snapshot id").Error(), log) @@ -210,35 +207,6 @@ func (c *podVolumeBackupController) processBackup(req *arkv1api.PodVolumeBackup) return nil } -// runCommand runs a command and returns its stdout, stderr, and its returned -// error (if any). If there are errors reading stdout or stderr, their return -// value(s) will contain the error as a string. -func runCommand(cmd *exec.Cmd) (string, string, error) { - stdoutBuf := new(bytes.Buffer) - stderrBuf := new(bytes.Buffer) - - cmd.Stdout = stdoutBuf - cmd.Stderr = stderrBuf - - runErr := cmd.Run() - - var stdout, stderr string - - if res, readErr := ioutil.ReadAll(stdoutBuf); readErr != nil { - stdout = errors.Wrap(readErr, "error reading command's stdout").Error() - } else { - stdout = string(res) - } - - if res, readErr := ioutil.ReadAll(stderrBuf); readErr != nil { - stderr = errors.Wrap(readErr, "error reading command's stderr").Error() - } else { - stderr = string(res) - } - - return stdout, stderr, runErr -} - func (c *podVolumeBackupController) patchPodVolumeBackup(req *arkv1api.PodVolumeBackup, mutate func(*arkv1api.PodVolumeBackup)) (*arkv1api.PodVolumeBackup, error) { // Record original json oldData, err := json.Marshal(req) diff --git a/pkg/controller/pod_volume_restore_controller.go b/pkg/controller/pod_volume_restore_controller.go index 6bc98afe1..eb3dd2e55 100644 --- a/pkg/controller/pod_volume_restore_controller.go +++ b/pkg/controller/pod_volume_restore_controller.go @@ -41,6 +41,7 @@ import ( listers "github.com/heptio/ark/pkg/generated/listers/ark/v1" "github.com/heptio/ark/pkg/restic" "github.com/heptio/ark/pkg/util/boolptr" + arkexec "github.com/heptio/ark/pkg/util/exec" "github.com/heptio/ark/pkg/util/kube" ) @@ -299,8 +300,7 @@ func restorePodVolume(req *arkv1api.PodVolumeRestore, credsFile, volumeDir strin } resticCmd := restic.RestoreCommand( - req.Spec.RepoPrefix, - req.Spec.Pod.Namespace, + req.Spec.RepoIdentifier, credsFile, req.Spec.SnapshotID, volumePath, @@ -308,7 +308,7 @@ func restorePodVolume(req *arkv1api.PodVolumeRestore, credsFile, volumeDir strin var stdout, stderr string - if stdout, stderr, err = runCommand(resticCmd.Cmd()); err != nil { + if stdout, stderr, err = arkexec.RunCommand(resticCmd.Cmd()); err != nil { return errors.Wrapf(err, "error running restic restore, cmd=%s, stdout=%s, stderr=%s", resticCmd.String(), stdout, stderr) } log.Debugf("Ran command=%s, stdout=%s, stderr=%s", resticCmd.String(), stdout, stderr) diff --git a/pkg/controller/restic_repository_controller.go b/pkg/controller/restic_repository_controller.go new file mode 100644 index 000000000..16378e60f --- /dev/null +++ b/pkg/controller/restic_repository_controller.go @@ -0,0 +1,276 @@ +/* +Copyright 2018 the Heptio Ark 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 controller + +import ( + "encoding/json" + "time" + + jsonpatch "github.com/evanphx/json-patch" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/clock" + "k8s.io/client-go/tools/cache" + + "github.com/heptio/ark/pkg/apis/ark/v1" + arkv1api "github.com/heptio/ark/pkg/apis/ark/v1" + arkv1client "github.com/heptio/ark/pkg/generated/clientset/versioned/typed/ark/v1" + informers "github.com/heptio/ark/pkg/generated/informers/externalversions/ark/v1" + listers "github.com/heptio/ark/pkg/generated/listers/ark/v1" + "github.com/heptio/ark/pkg/restic" +) + +type resticRepositoryController struct { + *genericController + + resticRepositoryClient arkv1client.ResticRepositoriesGetter + resticRepositoryLister listers.ResticRepositoryLister + objectStorageConfig arkv1api.ObjectStorageProviderConfig + repositoryManager restic.RepositoryManager + + clock clock.Clock +} + +// NewResticRepositoryController creates a new restic repository controller. +func NewResticRepositoryController( + logger logrus.FieldLogger, + resticRepositoryInformer informers.ResticRepositoryInformer, + resticRepositoryClient arkv1client.ResticRepositoriesGetter, + objectStorageConfig arkv1api.ObjectStorageProviderConfig, + repositoryManager restic.RepositoryManager, +) Interface { + c := &resticRepositoryController{ + genericController: newGenericController("restic-repository", logger), + resticRepositoryClient: resticRepositoryClient, + resticRepositoryLister: resticRepositoryInformer.Lister(), + objectStorageConfig: objectStorageConfig, + repositoryManager: repositoryManager, + clock: &clock.RealClock{}, + } + + c.syncHandler = c.processQueueItem + c.cacheSyncWaiters = append(c.cacheSyncWaiters, resticRepositoryInformer.Informer().HasSynced) + + resticRepositoryInformer.Informer().AddEventHandler( + cache.ResourceEventHandlerFuncs{ + AddFunc: c.enqueue, + }, + ) + + c.resyncPeriod = 30 * time.Minute + c.resyncFunc = c.enqueueAllRepositories + + return c +} + +// enqueueAllRepositories lists all restic repositories from cache and enqueues all +// of them so we can check each one for maintenance. +func (c *resticRepositoryController) enqueueAllRepositories() { + c.logger.Debug("resticRepositoryController.enqueueAllRepositories") + + repos, err := c.resticRepositoryLister.List(labels.Everything()) + if err != nil { + c.logger.WithError(errors.WithStack(err)).Error("error listing restic repositories") + return + } + + for _, repo := range repos { + c.enqueue(repo) + } +} + +func (c *resticRepositoryController) processQueueItem(key string) error { + log := c.logger.WithField("key", key) + log.Debug("Running processQueueItem") + + ns, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + log.WithError(errors.WithStack(err)).Error("error splitting queue key") + return nil + } + + log = c.logger.WithField("namespace", ns).WithField("name", name) + + req, err := c.resticRepositoryLister.ResticRepositories(ns).Get(name) + if apierrors.IsNotFound(err) { + log.Debug("Unable to find ResticRepository") + return nil + } + if err != nil { + return errors.Wrap(err, "error getting ResticRepository") + } + + // Don't mutate the shared cache + reqCopy := req.DeepCopy() + + switch req.Status.Phase { + case "", v1.ResticRepositoryPhaseNew: + return c.initializeRepo(reqCopy, log) + case v1.ResticRepositoryPhaseReady: + return c.runMaintenanceIfDue(reqCopy, log) + case v1.ResticRepositoryPhaseNotReady: + return c.checkNotReadyRepo(reqCopy, log) + } + + return nil +} + +func (c *resticRepositoryController) initializeRepo(req *v1.ResticRepository, log logrus.FieldLogger) error { + log.Info("Initializing restic repository") + + // defaulting - if the patch fails, return an error so the item is returned to the queue + if err := c.patchResticRepository(req, func(r *v1.ResticRepository) { + r.Spec.ResticIdentifier = restic.GetRepoIdentifier(c.objectStorageConfig, r.Name) + + if r.Spec.MaintenanceFrequency.Duration <= 0 { + r.Spec.MaintenanceFrequency = metav1.Duration{Duration: restic.DefaultMaintenanceFrequency} + } + }); err != nil { + return err + } + + if err := ensureRepo(req.Name, c.repositoryManager); err != nil { + return c.patchResticRepository(req, repoNotReady(err.Error())) + } + + return c.patchResticRepository(req, func(req *v1.ResticRepository) { + req.Status.Phase = v1.ResticRepositoryPhaseReady + req.Status.LastMaintenanceTime = metav1.Time{Time: time.Now()} + }) +} + +// ensureRepo first checks the repo, and returns if check passes. If it fails, +// attempts to init the repo, and returns the result. +func ensureRepo(name string, repoManager restic.RepositoryManager) error { + if repoManager.CheckRepo(name) == nil { + return nil + } + + return repoManager.InitRepo(name) +} + +func (c *resticRepositoryController) runMaintenanceIfDue(req *v1.ResticRepository, log logrus.FieldLogger) error { + log.Debug("resticRepositoryController.runMaintenanceIfDue") + + now := c.clock.Now() + + if !dueForMaintenance(req, now) { + log.Debug("not due for maintenance") + return nil + } + + log.Info("Running maintenance on restic repository") + + log.Debug("Checking repo before prune") + if err := c.repositoryManager.CheckRepo(req.Name); err != nil { + return c.patchResticRepository(req, repoNotReady(err.Error())) + } + + // prune failures should be displayed in the `.status.message` field but + // should not cause the repo to move to `NotReady`. + log.Debug("Pruning repo") + if err := c.repositoryManager.PruneRepo(req.Name); err != nil { + log.WithError(err).Warn("error pruning repository") + if patchErr := c.patchResticRepository(req, func(r *v1.ResticRepository) { + r.Status.Message = err.Error() + }); patchErr != nil { + return patchErr + } + } + + log.Debug("Checking repo after prune") + if err := c.repositoryManager.CheckRepo(req.Name); err != nil { + return c.patchResticRepository(req, repoNotReady(err.Error())) + } + + return c.patchResticRepository(req, func(req *v1.ResticRepository) { + req.Status.LastMaintenanceTime = metav1.Time{Time: now} + }) +} + +func dueForMaintenance(req *v1.ResticRepository, now time.Time) bool { + return req.Status.LastMaintenanceTime.Add(req.Spec.MaintenanceFrequency.Duration).Before(now) +} + +func (c *resticRepositoryController) checkNotReadyRepo(req *v1.ResticRepository, log logrus.FieldLogger) error { + log.Info("Checking restic repository for readiness") + + // we need to ensure it (first check, if check fails, attempt to init) + // because we don't know if it's been successfully initialized yet. + if err := ensureRepo(req.Name, c.repositoryManager); err != nil { + return c.patchResticRepository(req, repoNotReady(err.Error())) + } + + return c.patchResticRepository(req, repoReady()) +} + +func repoNotReady(msg string) func(*v1.ResticRepository) { + return func(r *v1.ResticRepository) { + r.Status.Phase = v1.ResticRepositoryPhaseNotReady + r.Status.Message = msg + } +} + +func repoReady() func(*v1.ResticRepository) { + return func(r *v1.ResticRepository) { + r.Status.Phase = v1.ResticRepositoryPhaseReady + r.Status.Message = "" + } +} + +// patchResticRepository mutates req with the provided mutate function, and patches it +// through the Kube API. After executing this function, req will be updated with both +// the mutation and the results of the Patch() API call. +func (c *resticRepositoryController) patchResticRepository(req *v1.ResticRepository, mutate func(*v1.ResticRepository)) error { + // Record original json + oldData, err := json.Marshal(req) + if err != nil { + return errors.Wrap(err, "error marshalling original ResticRepository") + } + + mutate(req) + + // Record new json + newData, err := json.Marshal(req) + if err != nil { + return errors.Wrap(err, "error marshalling updated ResticRepository") + } + + patchBytes, err := jsonpatch.CreateMergePatch(oldData, newData) + if err != nil { + return errors.Wrap(err, "error creating json merge patch for ResticRepository") + } + + // empty patch: don't apply + if string(patchBytes) == "{}" { + return nil + } + + // patch, and if successful, update req + var patched *v1.ResticRepository + if patched, err = c.resticRepositoryClient.ResticRepositories(req.Namespace).Patch(req.Name, types.MergePatchType, patchBytes); err != nil { + return errors.Wrap(err, "error patching ResticRepository") + } + req = patched + + return nil +} diff --git a/pkg/generated/clientset/versioned/typed/ark/v1/ark_client.go b/pkg/generated/clientset/versioned/typed/ark/v1/ark_client.go index 3caa51a9b..184634c47 100644 --- a/pkg/generated/clientset/versioned/typed/ark/v1/ark_client.go +++ b/pkg/generated/clientset/versioned/typed/ark/v1/ark_client.go @@ -33,6 +33,7 @@ type ArkV1Interface interface { DownloadRequestsGetter PodVolumeBackupsGetter PodVolumeRestoresGetter + ResticRepositoriesGetter RestoresGetter SchedulesGetter } @@ -66,6 +67,10 @@ func (c *ArkV1Client) PodVolumeRestores(namespace string) PodVolumeRestoreInterf return newPodVolumeRestores(c, namespace) } +func (c *ArkV1Client) ResticRepositories(namespace string) ResticRepositoryInterface { + return newResticRepositories(c, namespace) +} + func (c *ArkV1Client) Restores(namespace string) RestoreInterface { return newRestores(c, namespace) } diff --git a/pkg/generated/clientset/versioned/typed/ark/v1/fake/fake_ark_client.go b/pkg/generated/clientset/versioned/typed/ark/v1/fake/fake_ark_client.go index a06504732..25f8f0a2e 100644 --- a/pkg/generated/clientset/versioned/typed/ark/v1/fake/fake_ark_client.go +++ b/pkg/generated/clientset/versioned/typed/ark/v1/fake/fake_ark_client.go @@ -52,6 +52,10 @@ func (c *FakeArkV1) PodVolumeRestores(namespace string) v1.PodVolumeRestoreInter return &FakePodVolumeRestores{c, namespace} } +func (c *FakeArkV1) ResticRepositories(namespace string) v1.ResticRepositoryInterface { + return &FakeResticRepositories{c, namespace} +} + func (c *FakeArkV1) Restores(namespace string) v1.RestoreInterface { return &FakeRestores{c, namespace} } diff --git a/pkg/generated/clientset/versioned/typed/ark/v1/fake/fake_resticrepository.go b/pkg/generated/clientset/versioned/typed/ark/v1/fake/fake_resticrepository.go new file mode 100644 index 000000000..ae2b5ce55 --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/ark/v1/fake/fake_resticrepository.go @@ -0,0 +1,140 @@ +/* +Copyright 2018 the Heptio Ark 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + ark_v1 "github.com/heptio/ark/pkg/apis/ark/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeResticRepositories implements ResticRepositoryInterface +type FakeResticRepositories struct { + Fake *FakeArkV1 + ns string +} + +var resticrepositoriesResource = schema.GroupVersionResource{Group: "ark.heptio.com", Version: "v1", Resource: "resticrepositories"} + +var resticrepositoriesKind = schema.GroupVersionKind{Group: "ark.heptio.com", Version: "v1", Kind: "ResticRepository"} + +// Get takes name of the resticRepository, and returns the corresponding resticRepository object, and an error if there is any. +func (c *FakeResticRepositories) Get(name string, options v1.GetOptions) (result *ark_v1.ResticRepository, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(resticrepositoriesResource, c.ns, name), &ark_v1.ResticRepository{}) + + if obj == nil { + return nil, err + } + return obj.(*ark_v1.ResticRepository), err +} + +// List takes label and field selectors, and returns the list of ResticRepositories that match those selectors. +func (c *FakeResticRepositories) List(opts v1.ListOptions) (result *ark_v1.ResticRepositoryList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(resticrepositoriesResource, resticrepositoriesKind, c.ns, opts), &ark_v1.ResticRepositoryList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &ark_v1.ResticRepositoryList{} + for _, item := range obj.(*ark_v1.ResticRepositoryList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested resticRepositories. +func (c *FakeResticRepositories) Watch(opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(resticrepositoriesResource, c.ns, opts)) + +} + +// Create takes the representation of a resticRepository and creates it. Returns the server's representation of the resticRepository, and an error, if there is any. +func (c *FakeResticRepositories) Create(resticRepository *ark_v1.ResticRepository) (result *ark_v1.ResticRepository, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(resticrepositoriesResource, c.ns, resticRepository), &ark_v1.ResticRepository{}) + + if obj == nil { + return nil, err + } + return obj.(*ark_v1.ResticRepository), err +} + +// Update takes the representation of a resticRepository and updates it. Returns the server's representation of the resticRepository, and an error, if there is any. +func (c *FakeResticRepositories) Update(resticRepository *ark_v1.ResticRepository) (result *ark_v1.ResticRepository, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(resticrepositoriesResource, c.ns, resticRepository), &ark_v1.ResticRepository{}) + + if obj == nil { + return nil, err + } + return obj.(*ark_v1.ResticRepository), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeResticRepositories) UpdateStatus(resticRepository *ark_v1.ResticRepository) (*ark_v1.ResticRepository, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(resticrepositoriesResource, "status", c.ns, resticRepository), &ark_v1.ResticRepository{}) + + if obj == nil { + return nil, err + } + return obj.(*ark_v1.ResticRepository), err +} + +// Delete takes name of the resticRepository and deletes it. Returns an error if one occurs. +func (c *FakeResticRepositories) Delete(name string, options *v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(resticrepositoriesResource, c.ns, name), &ark_v1.ResticRepository{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeResticRepositories) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(resticrepositoriesResource, c.ns, listOptions) + + _, err := c.Fake.Invokes(action, &ark_v1.ResticRepositoryList{}) + return err +} + +// Patch applies the patch and returns the patched resticRepository. +func (c *FakeResticRepositories) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *ark_v1.ResticRepository, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(resticrepositoriesResource, c.ns, name, data, subresources...), &ark_v1.ResticRepository{}) + + if obj == nil { + return nil, err + } + return obj.(*ark_v1.ResticRepository), err +} diff --git a/pkg/generated/clientset/versioned/typed/ark/v1/generated_expansion.go b/pkg/generated/clientset/versioned/typed/ark/v1/generated_expansion.go index ee1cb0bdd..e09577a39 100644 --- a/pkg/generated/clientset/versioned/typed/ark/v1/generated_expansion.go +++ b/pkg/generated/clientset/versioned/typed/ark/v1/generated_expansion.go @@ -30,6 +30,8 @@ type PodVolumeBackupExpansion interface{} type PodVolumeRestoreExpansion interface{} +type ResticRepositoryExpansion interface{} + type RestoreExpansion interface{} type ScheduleExpansion interface{} diff --git a/pkg/generated/clientset/versioned/typed/ark/v1/resticrepository.go b/pkg/generated/clientset/versioned/typed/ark/v1/resticrepository.go new file mode 100644 index 000000000..1c1512095 --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/ark/v1/resticrepository.go @@ -0,0 +1,174 @@ +/* +Copyright 2018 the Heptio Ark 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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1 + +import ( + v1 "github.com/heptio/ark/pkg/apis/ark/v1" + scheme "github.com/heptio/ark/pkg/generated/clientset/versioned/scheme" + meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// ResticRepositoriesGetter has a method to return a ResticRepositoryInterface. +// A group's client should implement this interface. +type ResticRepositoriesGetter interface { + ResticRepositories(namespace string) ResticRepositoryInterface +} + +// ResticRepositoryInterface has methods to work with ResticRepository resources. +type ResticRepositoryInterface interface { + Create(*v1.ResticRepository) (*v1.ResticRepository, error) + Update(*v1.ResticRepository) (*v1.ResticRepository, error) + UpdateStatus(*v1.ResticRepository) (*v1.ResticRepository, error) + Delete(name string, options *meta_v1.DeleteOptions) error + DeleteCollection(options *meta_v1.DeleteOptions, listOptions meta_v1.ListOptions) error + Get(name string, options meta_v1.GetOptions) (*v1.ResticRepository, error) + List(opts meta_v1.ListOptions) (*v1.ResticRepositoryList, error) + Watch(opts meta_v1.ListOptions) (watch.Interface, error) + Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1.ResticRepository, err error) + ResticRepositoryExpansion +} + +// resticRepositories implements ResticRepositoryInterface +type resticRepositories struct { + client rest.Interface + ns string +} + +// newResticRepositories returns a ResticRepositories +func newResticRepositories(c *ArkV1Client, namespace string) *resticRepositories { + return &resticRepositories{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the resticRepository, and returns the corresponding resticRepository object, and an error if there is any. +func (c *resticRepositories) Get(name string, options meta_v1.GetOptions) (result *v1.ResticRepository, err error) { + result = &v1.ResticRepository{} + err = c.client.Get(). + Namespace(c.ns). + Resource("resticrepositories"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of ResticRepositories that match those selectors. +func (c *resticRepositories) List(opts meta_v1.ListOptions) (result *v1.ResticRepositoryList, err error) { + result = &v1.ResticRepositoryList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("resticrepositories"). + VersionedParams(&opts, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested resticRepositories. +func (c *resticRepositories) Watch(opts meta_v1.ListOptions) (watch.Interface, error) { + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("resticrepositories"). + VersionedParams(&opts, scheme.ParameterCodec). + Watch() +} + +// Create takes the representation of a resticRepository and creates it. Returns the server's representation of the resticRepository, and an error, if there is any. +func (c *resticRepositories) Create(resticRepository *v1.ResticRepository) (result *v1.ResticRepository, err error) { + result = &v1.ResticRepository{} + err = c.client.Post(). + Namespace(c.ns). + Resource("resticrepositories"). + Body(resticRepository). + Do(). + Into(result) + return +} + +// Update takes the representation of a resticRepository and updates it. Returns the server's representation of the resticRepository, and an error, if there is any. +func (c *resticRepositories) Update(resticRepository *v1.ResticRepository) (result *v1.ResticRepository, err error) { + result = &v1.ResticRepository{} + err = c.client.Put(). + Namespace(c.ns). + Resource("resticrepositories"). + Name(resticRepository.Name). + Body(resticRepository). + Do(). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + +func (c *resticRepositories) UpdateStatus(resticRepository *v1.ResticRepository) (result *v1.ResticRepository, err error) { + result = &v1.ResticRepository{} + err = c.client.Put(). + Namespace(c.ns). + Resource("resticrepositories"). + Name(resticRepository.Name). + SubResource("status"). + Body(resticRepository). + Do(). + Into(result) + return +} + +// Delete takes name of the resticRepository and deletes it. Returns an error if one occurs. +func (c *resticRepositories) Delete(name string, options *meta_v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("resticrepositories"). + Name(name). + Body(options). + Do(). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *resticRepositories) DeleteCollection(options *meta_v1.DeleteOptions, listOptions meta_v1.ListOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("resticrepositories"). + VersionedParams(&listOptions, scheme.ParameterCodec). + Body(options). + Do(). + Error() +} + +// Patch applies the patch and returns the patched resticRepository. +func (c *resticRepositories) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1.ResticRepository, err error) { + result = &v1.ResticRepository{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("resticrepositories"). + SubResource(subresources...). + Name(name). + Body(data). + Do(). + Into(result) + return +} diff --git a/pkg/generated/informers/externalversions/ark/v1/interface.go b/pkg/generated/informers/externalversions/ark/v1/interface.go index c1d0e69d2..b197b2a66 100644 --- a/pkg/generated/informers/externalversions/ark/v1/interface.go +++ b/pkg/generated/informers/externalversions/ark/v1/interface.go @@ -36,6 +36,8 @@ type Interface interface { PodVolumeBackups() PodVolumeBackupInformer // PodVolumeRestores returns a PodVolumeRestoreInformer. PodVolumeRestores() PodVolumeRestoreInformer + // ResticRepositories returns a ResticRepositoryInformer. + ResticRepositories() ResticRepositoryInformer // Restores returns a RestoreInformer. Restores() RestoreInformer // Schedules returns a ScheduleInformer. @@ -83,6 +85,11 @@ func (v *version) PodVolumeRestores() PodVolumeRestoreInformer { return &podVolumeRestoreInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// ResticRepositories returns a ResticRepositoryInformer. +func (v *version) ResticRepositories() ResticRepositoryInformer { + return &resticRepositoryInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // Restores returns a RestoreInformer. func (v *version) Restores() RestoreInformer { return &restoreInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/pkg/generated/informers/externalversions/ark/v1/resticrepository.go b/pkg/generated/informers/externalversions/ark/v1/resticrepository.go new file mode 100644 index 000000000..f30004e53 --- /dev/null +++ b/pkg/generated/informers/externalversions/ark/v1/resticrepository.go @@ -0,0 +1,89 @@ +/* +Copyright 2018 the Heptio Ark 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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1 + +import ( + time "time" + + ark_v1 "github.com/heptio/ark/pkg/apis/ark/v1" + versioned "github.com/heptio/ark/pkg/generated/clientset/versioned" + internalinterfaces "github.com/heptio/ark/pkg/generated/informers/externalversions/internalinterfaces" + v1 "github.com/heptio/ark/pkg/generated/listers/ark/v1" + meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// ResticRepositoryInformer provides access to a shared informer and lister for +// ResticRepositories. +type ResticRepositoryInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1.ResticRepositoryLister +} + +type resticRepositoryInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewResticRepositoryInformer constructs a new informer for ResticRepository type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewResticRepositoryInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredResticRepositoryInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredResticRepositoryInformer constructs a new informer for ResticRepository type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredResticRepositoryInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options meta_v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.ArkV1().ResticRepositories(namespace).List(options) + }, + WatchFunc: func(options meta_v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.ArkV1().ResticRepositories(namespace).Watch(options) + }, + }, + &ark_v1.ResticRepository{}, + resyncPeriod, + indexers, + ) +} + +func (f *resticRepositoryInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredResticRepositoryInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *resticRepositoryInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&ark_v1.ResticRepository{}, f.defaultInformer) +} + +func (f *resticRepositoryInformer) Lister() v1.ResticRepositoryLister { + return v1.NewResticRepositoryLister(f.Informer().GetIndexer()) +} diff --git a/pkg/generated/informers/externalversions/generic.go b/pkg/generated/informers/externalversions/generic.go index 4ad3b38d6..6e1c22334 100644 --- a/pkg/generated/informers/externalversions/generic.go +++ b/pkg/generated/informers/externalversions/generic.go @@ -65,6 +65,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Ark().V1().PodVolumeBackups().Informer()}, nil case v1.SchemeGroupVersion.WithResource("podvolumerestores"): return &genericInformer{resource: resource.GroupResource(), informer: f.Ark().V1().PodVolumeRestores().Informer()}, nil + case v1.SchemeGroupVersion.WithResource("resticrepositories"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Ark().V1().ResticRepositories().Informer()}, nil case v1.SchemeGroupVersion.WithResource("restores"): return &genericInformer{resource: resource.GroupResource(), informer: f.Ark().V1().Restores().Informer()}, nil case v1.SchemeGroupVersion.WithResource("schedules"): diff --git a/pkg/generated/listers/ark/v1/expansion_generated.go b/pkg/generated/listers/ark/v1/expansion_generated.go index 9b5a03f5d..9b4a1d5aa 100644 --- a/pkg/generated/listers/ark/v1/expansion_generated.go +++ b/pkg/generated/listers/ark/v1/expansion_generated.go @@ -66,6 +66,14 @@ type PodVolumeRestoreListerExpansion interface{} // PodVolumeRestoreNamespaceLister. type PodVolumeRestoreNamespaceListerExpansion interface{} +// ResticRepositoryListerExpansion allows custom methods to be added to +// ResticRepositoryLister. +type ResticRepositoryListerExpansion interface{} + +// ResticRepositoryNamespaceListerExpansion allows custom methods to be added to +// ResticRepositoryNamespaceLister. +type ResticRepositoryNamespaceListerExpansion interface{} + // RestoreListerExpansion allows custom methods to be added to // RestoreLister. type RestoreListerExpansion interface{} diff --git a/pkg/generated/listers/ark/v1/resticrepository.go b/pkg/generated/listers/ark/v1/resticrepository.go new file mode 100644 index 000000000..9fc95fa29 --- /dev/null +++ b/pkg/generated/listers/ark/v1/resticrepository.go @@ -0,0 +1,94 @@ +/* +Copyright 2018 the Heptio Ark 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. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1 + +import ( + v1 "github.com/heptio/ark/pkg/apis/ark/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// ResticRepositoryLister helps list ResticRepositories. +type ResticRepositoryLister interface { + // List lists all ResticRepositories in the indexer. + List(selector labels.Selector) (ret []*v1.ResticRepository, err error) + // ResticRepositories returns an object that can list and get ResticRepositories. + ResticRepositories(namespace string) ResticRepositoryNamespaceLister + ResticRepositoryListerExpansion +} + +// resticRepositoryLister implements the ResticRepositoryLister interface. +type resticRepositoryLister struct { + indexer cache.Indexer +} + +// NewResticRepositoryLister returns a new ResticRepositoryLister. +func NewResticRepositoryLister(indexer cache.Indexer) ResticRepositoryLister { + return &resticRepositoryLister{indexer: indexer} +} + +// List lists all ResticRepositories in the indexer. +func (s *resticRepositoryLister) List(selector labels.Selector) (ret []*v1.ResticRepository, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1.ResticRepository)) + }) + return ret, err +} + +// ResticRepositories returns an object that can list and get ResticRepositories. +func (s *resticRepositoryLister) ResticRepositories(namespace string) ResticRepositoryNamespaceLister { + return resticRepositoryNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// ResticRepositoryNamespaceLister helps list and get ResticRepositories. +type ResticRepositoryNamespaceLister interface { + // List lists all ResticRepositories in the indexer for a given namespace. + List(selector labels.Selector) (ret []*v1.ResticRepository, err error) + // Get retrieves the ResticRepository from the indexer for a given namespace and name. + Get(name string) (*v1.ResticRepository, error) + ResticRepositoryNamespaceListerExpansion +} + +// resticRepositoryNamespaceLister implements the ResticRepositoryNamespaceLister +// interface. +type resticRepositoryNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all ResticRepositories in the indexer for a given namespace. +func (s resticRepositoryNamespaceLister) List(selector labels.Selector) (ret []*v1.ResticRepository, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1.ResticRepository)) + }) + return ret, err +} + +// Get retrieves the ResticRepository from the indexer for a given namespace and name. +func (s resticRepositoryNamespaceLister) Get(name string) (*v1.ResticRepository, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1.Resource("resticrepository"), name) + } + return obj.(*v1.ResticRepository), nil +} diff --git a/pkg/install/config.go b/pkg/install/config.go index c3f896d16..93690d61b 100644 --- a/pkg/install/config.go +++ b/pkg/install/config.go @@ -31,6 +31,7 @@ type arkConfig struct { gcSyncPeriod time.Duration podVolumeOperationTimeout time.Duration restoreOnly bool + resticLocation string } func WithBackupSyncPeriod(t time.Duration) arkConfigOption { @@ -57,6 +58,12 @@ func WithRestoreOnly() arkConfigOption { } } +func WithResticLocation(location string) arkConfigOption { + return func(c *arkConfig) { + c.resticLocation = location + } +} + func Config( namespace string, pvCloudProviderName string, @@ -87,7 +94,8 @@ func Config( Name: backupCloudProviderName, Config: backupCloudProviderConfig, }, - Bucket: bucket, + Bucket: bucket, + ResticLocation: c.resticLocation, }, BackupSyncPeriod: metav1.Duration{ Duration: c.backupSyncPeriod, diff --git a/pkg/install/crd.go b/pkg/install/crd.go index 1e960c704..2f7a9a6b0 100644 --- a/pkg/install/crd.go +++ b/pkg/install/crd.go @@ -36,6 +36,7 @@ func CRDs() []*apiextv1beta1.CustomResourceDefinition { crd("DeleteBackupRequest", "deletebackuprequests"), crd("PodVolumeBackup", "podvolumebackups"), crd("PodVolumeRestore", "podvolumerestores"), + crd("ResticRepository", "resticrepositories"), } } diff --git a/pkg/restic/backupper.go b/pkg/restic/backupper.go index 3edcc93f9..ec8672c33 100644 --- a/pkg/restic/backupper.go +++ b/pkg/restic/backupper.go @@ -25,10 +25,12 @@ import ( "github.com/sirupsen/logrus" corev1api "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/cache" arkv1api "github.com/heptio/ark/pkg/apis/ark/v1" + arkv1listers "github.com/heptio/ark/pkg/generated/listers/ark/v1" "github.com/heptio/ark/pkg/util/boolptr" ) @@ -39,18 +41,26 @@ type Backupper interface { } type backupper struct { - repoManager *repositoryManager ctx context.Context + repoManager *repositoryManager + repoLister arkv1listers.ResticRepositoryLister results map[string]chan *arkv1api.PodVolumeBackup resultsLock sync.Mutex } -func newBackupper(ctx context.Context, repoManager *repositoryManager, podVolumeBackupInformer cache.SharedIndexInformer) *backupper { +func newBackupper( + ctx context.Context, + repoManager *repositoryManager, + podVolumeBackupInformer cache.SharedIndexInformer, + repoLister arkv1listers.ResticRepositoryLister, +) *backupper { b := &backupper{ - repoManager: repoManager, ctx: ctx, - results: make(map[string]chan *arkv1api.PodVolumeBackup), + repoManager: repoManager, + repoLister: repoLister, + + results: make(map[string]chan *arkv1api.PodVolumeBackup), } podVolumeBackupInformer.AddEventHandler( @@ -74,6 +84,31 @@ func resultsKey(ns, name string) string { return fmt.Sprintf("%s/%s", ns, name) } +func getRepo(repoLister arkv1listers.ResticRepositoryLister, ns, name string) (*arkv1api.ResticRepository, error) { + repo, err := repoLister.ResticRepositories(ns).Get(name) + if apierrors.IsNotFound(err) { + return nil, errors.Wrapf(err, "restic repository not found") + } + if err != nil { + return nil, errors.Wrapf(err, "error getting restic repository") + } + + return repo, nil +} + +func getReadyRepo(repoLister arkv1listers.ResticRepositoryLister, ns, name string) (*arkv1api.ResticRepository, error) { + repo, err := getRepo(repoLister, ns, name) + if err != nil { + return nil, err + } + + if repo.Status.Phase != arkv1api.ResticRepositoryPhaseReady { + return nil, errors.New("restic repository not ready") + } + + return repo, nil +} + func (b *backupper) BackupPodVolumes(backup *arkv1api.Backup, pod *corev1api.Pod, log logrus.FieldLogger) (map[string]string, []error) { // get volumes to backup from pod's annotations volumesToBackup := GetVolumesToBackup(pod) @@ -81,8 +116,8 @@ func (b *backupper) BackupPodVolumes(backup *arkv1api.Backup, pod *corev1api.Pod return nil, nil } - // ensure a repo exists for the pod's namespace - if err := b.repoManager.ensureRepo(pod.Namespace); err != nil { + repo, err := getReadyRepo(b.repoLister, backup.Namespace, pod.Namespace) + if err != nil { return nil, []error{err} } @@ -101,7 +136,7 @@ func (b *backupper) BackupPodVolumes(backup *arkv1api.Backup, pod *corev1api.Pod b.repoManager.repoLocker.Lock(pod.Namespace) defer b.repoManager.repoLocker.Unlock(pod.Namespace) - volumeBackup := newPodVolumeBackup(backup, pod, volumeName, b.repoManager.config.repoPrefix) + volumeBackup := newPodVolumeBackup(backup, pod, volumeName, repo.Spec.ResticIdentifier) if err := errorOnly(b.repoManager.arkClient.ArkV1().PodVolumeBackups(volumeBackup.Namespace).Create(volumeBackup)); err != nil { errs = append(errs, err) @@ -135,7 +170,7 @@ ForEachVolume: return volumeSnapshots, errs } -func newPodVolumeBackup(backup *arkv1api.Backup, pod *corev1api.Pod, volumeName, repoPrefix string) *arkv1api.PodVolumeBackup { +func newPodVolumeBackup(backup *arkv1api.Backup, pod *corev1api.Pod, volumeName, repoIdentifier string) *arkv1api.PodVolumeBackup { return &arkv1api.PodVolumeBackup{ ObjectMeta: metav1.ObjectMeta{ Namespace: backup.Namespace, @@ -171,7 +206,11 @@ func newPodVolumeBackup(backup *arkv1api.Backup, pod *corev1api.Pod, volumeName, "ns": pod.Namespace, "volume": volumeName, }, - RepoPrefix: repoPrefix, + RepoIdentifier: repoIdentifier, }, } } + +func errorOnly(_ interface{}, err error) error { + return err +} diff --git a/pkg/restic/command.go b/pkg/restic/command.go index 99e0ed321..705854ece 100644 --- a/pkg/restic/command.go +++ b/pkg/restic/command.go @@ -18,29 +18,46 @@ package restic import ( "fmt" + "os" "os/exec" + "path/filepath" "strings" ) // Command represents a restic command. type Command struct { - Command string - RepoPrefix string - Repo string - PasswordFile string - Dir string - Args []string - ExtraFlags []string + Command string + RepoIdentifier string + PasswordFile string + Dir string + Args []string + ExtraFlags []string +} + +func (c *Command) RepoName() string { + if c.RepoIdentifier == "" { + return "" + } + + return c.RepoIdentifier[strings.LastIndex(c.RepoIdentifier, "/")+1:] } // StringSlice returns the command as a slice of strings. func (c *Command) StringSlice() []string { res := []string{"restic"} - res = append(res, c.Command, repoFlag(c.RepoPrefix, c.Repo)) + res = append(res, c.Command, repoFlag(c.RepoIdentifier)) if c.PasswordFile != "" { res = append(res, passwordFlag(c.PasswordFile)) } + + // If ARK_SCRATCH_DIR is defined, put the restic cache within it. If not, + // allow restic to choose the location. This makes running either in-cluster + // or local (dev) work properly. + if scratch := os.Getenv("ARK_SCRATCH_DIR"); scratch != "" { + res = append(res, cacheDirFlag(filepath.Join(scratch, ".cache", "restic"))) + } + res = append(res, c.Args...) res = append(res, c.ExtraFlags...) @@ -61,10 +78,14 @@ func (c *Command) Cmd() *exec.Cmd { return cmd } -func repoFlag(prefix, repo string) string { - return fmt.Sprintf("--repo=%s/%s", prefix, repo) +func repoFlag(repoIdentifier string) string { + return fmt.Sprintf("--repo=%s", repoIdentifier) } func passwordFlag(file string) string { return fmt.Sprintf("--password-file=%s", file) } + +func cacheDirFlag(dir string) string { + return fmt.Sprintf("--cache-dir=%s", dir) +} diff --git a/pkg/restic/command_factory.go b/pkg/restic/command_factory.go index a0d06a9f2..df6fcaa8d 100644 --- a/pkg/restic/command_factory.go +++ b/pkg/restic/command_factory.go @@ -6,15 +6,14 @@ import ( ) // BackupCommand returns a Command for running a restic backup. -func BackupCommand(repoPrefix, repo, passwordFile, path string, tags map[string]string) *Command { +func BackupCommand(repoIdentifier, passwordFile, path string, tags map[string]string) *Command { return &Command{ - Command: "backup", - RepoPrefix: repoPrefix, - Repo: repo, - PasswordFile: passwordFile, - Dir: path, - Args: []string{"."}, - ExtraFlags: backupTagFlags(tags), + Command: "backup", + RepoIdentifier: repoIdentifier, + PasswordFile: passwordFile, + Dir: path, + Args: []string{"."}, + ExtraFlags: backupTagFlags(tags), } } @@ -27,26 +26,24 @@ func backupTagFlags(tags map[string]string) []string { } // RestoreCommand returns a Command for running a restic restore. -func RestoreCommand(repoPrefix, repo, passwordFile, snapshotID, target string) *Command { +func RestoreCommand(repoIdentifier, passwordFile, snapshotID, target string) *Command { return &Command{ - Command: "restore", - RepoPrefix: repoPrefix, - Repo: repo, - PasswordFile: passwordFile, - Dir: target, - Args: []string{snapshotID}, - ExtraFlags: []string{"--target=."}, + Command: "restore", + RepoIdentifier: repoIdentifier, + PasswordFile: passwordFile, + Dir: target, + Args: []string{snapshotID}, + ExtraFlags: []string{"--target=."}, } } // GetSnapshotCommand returns a Command for running a restic (get) snapshots. -func GetSnapshotCommand(repoPrefix, repo, passwordFile string, tags map[string]string) *Command { +func GetSnapshotCommand(repoIdentifier, passwordFile string, tags map[string]string) *Command { return &Command{ - Command: "snapshots", - RepoPrefix: repoPrefix, - Repo: repo, - PasswordFile: passwordFile, - ExtraFlags: []string{"--json", "--last", getSnapshotTagFlag(tags)}, + Command: "snapshots", + RepoIdentifier: repoIdentifier, + PasswordFile: passwordFile, + ExtraFlags: []string{"--json", "--last", getSnapshotTagFlag(tags)}, } } @@ -59,35 +56,31 @@ func getSnapshotTagFlag(tags map[string]string) string { return fmt.Sprintf("--tag=%s", strings.Join(tagFilters, ",")) } -func InitCommand(repoPrefix, repo string) *Command { +func InitCommand(repoIdentifier string) *Command { return &Command{ - Command: "init", - RepoPrefix: repoPrefix, - Repo: repo, + Command: "init", + RepoIdentifier: repoIdentifier, } } -func CheckCommand(repoPrefix, repo string) *Command { +func CheckCommand(repoIdentifier string) *Command { return &Command{ - Command: "check", - RepoPrefix: repoPrefix, - Repo: repo, + Command: "check", + RepoIdentifier: repoIdentifier, } } -func PruneCommand(repoPrefix, repo string) *Command { +func PruneCommand(repoIdentifier string) *Command { return &Command{ - Command: "prune", - RepoPrefix: repoPrefix, - Repo: repo, + Command: "prune", + RepoIdentifier: repoIdentifier, } } -func ForgetCommand(repoPrefix, repo, snapshotID string) *Command { +func ForgetCommand(repoIdentifier, snapshotID string) *Command { return &Command{ - Command: "forget", - RepoPrefix: repoPrefix, - Repo: repo, - Args: []string{snapshotID}, + Command: "forget", + RepoIdentifier: repoIdentifier, + Args: []string{snapshotID}, } } diff --git a/pkg/restic/common.go b/pkg/restic/common.go index 02c654d31..b6dd3ebaa 100644 --- a/pkg/restic/common.go +++ b/pkg/restic/common.go @@ -20,6 +20,7 @@ import ( "fmt" "io/ioutil" "strings" + "time" "github.com/pkg/errors" @@ -32,7 +33,8 @@ import ( ) const ( - InitContainer = "restic-wait" + InitContainer = "restic-wait" + DefaultMaintenanceFrequency = 24 * time.Hour podAnnotationPrefix = "snapshot.ark.heptio.com/" volumesToBackupAnnotation = "backup.ark.heptio.com/backup-volumes" diff --git a/pkg/restic/config.go b/pkg/restic/config.go new file mode 100644 index 000000000..9ab8891e7 --- /dev/null +++ b/pkg/restic/config.go @@ -0,0 +1,79 @@ +/* +Copyright 2018 the Heptio Ark 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 restic + +import ( + "fmt" + "strings" + + arkv1api "github.com/heptio/ark/pkg/apis/ark/v1" +) + +type BackendType string + +const ( + AWSBackend BackendType = "aws" + AzureBackend BackendType = "azure" + GCPBackend BackendType = "gcp" +) + +// getRepoPrefix returns the prefix of the value of the --repo flag for +// restic commands, i.e. everything except the "/". +func getRepoPrefix(config arkv1api.ObjectStorageProviderConfig) string { + if BackendType(config.Name) == AWSBackend { + var url string + switch { + // non-AWS, S3-compatible object store + case config.Config["s3Url"] != "": + url = config.Config["s3Url"] + default: + url = "s3.amazonaws.com" + } + + return fmt.Sprintf("s3:%s/%s", url, config.ResticLocation) + } + + var ( + parts = strings.SplitN(config.ResticLocation, "/", 2) + bucket, path string + ) + + if len(parts) >= 1 { + bucket = parts[0] + } + if len(parts) >= 2 { + path = parts[1] + } + + var prefix string + switch BackendType(config.Name) { + case AzureBackend: + prefix = "azure" + case GCPBackend: + prefix = "gs" + } + + return fmt.Sprintf("%s:%s:/%s", prefix, bucket, path) +} + +// GetRepoIdentifier returns the string to be used as the value of the --repo flag in +// restic commands for the given repository. +func GetRepoIdentifier(config arkv1api.ObjectStorageProviderConfig, name string) string { + prefix := getRepoPrefix(config) + + return fmt.Sprintf("%s/%s", strings.TrimSuffix(prefix, "/"), name) +} diff --git a/pkg/restic/exec_commands.go b/pkg/restic/exec_commands.go index 9dca38b1a..cb5937135 100644 --- a/pkg/restic/exec_commands.go +++ b/pkg/restic/exec_commands.go @@ -10,8 +10,8 @@ import ( // GetSnapshotID runs a 'restic snapshots' command to get the ID of the snapshot // in the specified repo matching the set of provided tags, or an error if a // unique snapshot cannot be identified. -func GetSnapshotID(repoPrefix, repo, passwordFile string, tags map[string]string) (string, error) { - output, err := GetSnapshotCommand(repoPrefix, repo, passwordFile, tags).Cmd().Output() +func GetSnapshotID(repoIdentifier, passwordFile string, tags map[string]string) (string, error) { + output, err := GetSnapshotCommand(repoIdentifier, passwordFile, tags).Cmd().Output() if err != nil { if exitErr, ok := err.(*exec.ExitError); ok { return "", errors.Wrapf(err, "error running command, stderr=%s", exitErr.Stderr) diff --git a/pkg/restic/repository_manager.go b/pkg/restic/repository_manager.go index 064e13483..b714eac4d 100644 --- a/pkg/restic/repository_manager.go +++ b/pkg/restic/repository_manager.go @@ -20,40 +20,33 @@ import ( "context" "fmt" "os" - "os/exec" - "strings" "github.com/pkg/errors" "github.com/sirupsen/logrus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - kerrs "k8s.io/apimachinery/pkg/util/errors" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" corev1listers "k8s.io/client-go/listers/core/v1" "k8s.io/client-go/tools/cache" arkv1api "github.com/heptio/ark/pkg/apis/ark/v1" - "github.com/heptio/ark/pkg/cloudprovider" clientset "github.com/heptio/ark/pkg/generated/clientset/versioned" arkv1informers "github.com/heptio/ark/pkg/generated/informers/externalversions/ark/v1" - "github.com/heptio/ark/pkg/util/sync" + arkv1listers "github.com/heptio/ark/pkg/generated/listers/ark/v1" + arkexec "github.com/heptio/ark/pkg/util/exec" ) // RepositoryManager executes commands against restic repositories. type RepositoryManager interface { + // InitRepo initializes a repo with the specified name. + InitRepo(name string) error + // CheckRepo checks the specified repo for errors. CheckRepo(name string) error - // CheckAllRepos checks all repos for errors. - CheckAllRepos() error - // PruneRepo deletes unused data from a repo. PruneRepo(name string) error - // PruneAllRepos deletes unused data from all - // repos. - PruneAllRepos() error - // Forget removes a snapshot from the list of // available snapshots in a repo. Forget(snapshot SnapshotIdentifier) error @@ -77,87 +70,36 @@ type RestorerFactory interface { NewRestorer(context.Context, *arkv1api.Restore) (Restorer, error) } -type BackendType string - -const ( - AWSBackend BackendType = "aws" - AzureBackend BackendType = "azure" - GCPBackend BackendType = "gcp" -) - type repositoryManager struct { - objectStore cloudprovider.ObjectStore - config config - arkClient clientset.Interface - secretsLister corev1listers.SecretLister - secretsClient corev1client.SecretsGetter - log logrus.FieldLogger - repoLocker *repoLocker -} - -type config struct { - repoPrefix string - bucket string - path string -} - -func getConfig(objectStorageConfig arkv1api.ObjectStorageProviderConfig) config { - var ( - c = config{} - parts = strings.SplitN(objectStorageConfig.ResticLocation, "/", 2) - ) - - switch len(parts) { - case 0: - case 1: - c.bucket = parts[0] - default: - c.bucket = parts[0] - c.path = parts[1] - } - - switch BackendType(objectStorageConfig.Name) { - case AWSBackend: - var url string - switch { - // non-AWS, S3-compatible object store - case objectStorageConfig.Config != nil && objectStorageConfig.Config["s3Url"] != "": - url = objectStorageConfig.Config["s3Url"] - default: - url = "s3.amazonaws.com" - } - - c.repoPrefix = fmt.Sprintf("s3:%s/%s", url, c.bucket) - if c.path != "" { - c.repoPrefix += "/" + c.path - } - case AzureBackend: - c.repoPrefix = fmt.Sprintf("azure:%s:/%s", c.bucket, c.path) - case GCPBackend: - c.repoPrefix = fmt.Sprintf("gs:%s:/%s", c.bucket, c.path) - } - - return c + namespace string + arkClient clientset.Interface + secretsLister corev1listers.SecretLister + secretsClient corev1client.SecretsGetter + repoLister arkv1listers.ResticRepositoryLister + repoInformerSynced cache.InformerSynced + log logrus.FieldLogger + repoLocker *repoLocker } // NewRepositoryManager constructs a RepositoryManager. func NewRepositoryManager( ctx context.Context, - objectStore cloudprovider.ObjectStore, - config arkv1api.ObjectStorageProviderConfig, + namespace string, arkClient clientset.Interface, secretsInformer cache.SharedIndexInformer, secretsClient corev1client.SecretsGetter, + repoInformer arkv1informers.ResticRepositoryInformer, log logrus.FieldLogger, ) (RepositoryManager, error) { rm := &repositoryManager{ - objectStore: objectStore, - config: getConfig(config), - arkClient: arkClient, - secretsLister: corev1listers.NewSecretLister(secretsInformer.GetIndexer()), - secretsClient: secretsClient, - log: log, - repoLocker: newRepoLocker(), + namespace: namespace, + arkClient: arkClient, + secretsLister: corev1listers.NewSecretLister(secretsInformer.GetIndexer()), + secretsClient: secretsClient, + repoLister: repoInformer.Lister(), + repoInformerSynced: repoInformer.Informer().HasSynced, + log: log, + repoLocker: newRepoLocker(), } if !cache.WaitForCacheSync(ctx.Done(), secretsInformer.HasSynced) { @@ -178,11 +120,11 @@ func (rm *repositoryManager) NewBackupper(ctx context.Context, backup *arkv1api. }, ) - b := newBackupper(ctx, rm, informer) + b := newBackupper(ctx, rm, informer, rm.repoLister) go informer.Run(ctx.Done()) - if !cache.WaitForCacheSync(ctx.Done(), informer.HasSynced) { - return nil, errors.New("timed out waiting for cache to sync") + if !cache.WaitForCacheSync(ctx.Done(), informer.HasSynced, rm.repoInformerSynced) { + return nil, errors.New("timed out waiting for caches to sync") } return b, nil @@ -199,7 +141,7 @@ func (rm *repositoryManager) NewRestorer(ctx context.Context, restore *arkv1api. }, ) - r := newRestorer(ctx, rm, informer) + r := newRestorer(ctx, rm, informer, rm.repoLister) go informer.Run(ctx.Done()) if !cache.WaitForCacheSync(ctx.Done(), informer.HasSynced) { @@ -209,142 +151,80 @@ func (rm *repositoryManager) NewRestorer(ctx context.Context, restore *arkv1api. return r, nil } -func (rm *repositoryManager) ensureRepo(name string) error { - repos, err := rm.getAllRepos() +func (rm *repositoryManager) InitRepo(name string) error { + repo, err := getRepo(rm.repoLister, rm.namespace, name) if err != nil { return err } - for _, repo := range repos { - if repo == name { - return nil - } - } - rm.repoLocker.LockExclusive(name) defer rm.repoLocker.UnlockExclusive(name) - // init the repo - cmd := InitCommand(rm.config.repoPrefix, name) - - return errorOnly(rm.exec(cmd)) -} - -func (rm *repositoryManager) getAllRepos() ([]string, error) { - // TODO support rm.config.path - prefixes, err := rm.objectStore.ListCommonPrefixes(rm.config.bucket, "/") - if err != nil { - return nil, err - } - - var repos []string - for _, prefix := range prefixes { - if len(prefix) <= 1 { - continue - } - - // strip the trailing '/' if it exists - repos = append(repos, strings.TrimSuffix(prefix, "/")) - } - - return repos, nil -} - -func (rm *repositoryManager) CheckAllRepos() error { - repos, err := rm.getAllRepos() - if err != nil { - return err - } - - var eg sync.ErrorGroup - for _, repo := range repos { - this := repo - eg.Go(func() error { - rm.log.WithField("repo", this).Debugf("Checking repo %s", this) - return rm.CheckRepo(this) - }) - } - - return kerrs.NewAggregate(eg.Wait()) -} - -func (rm *repositoryManager) PruneAllRepos() error { - repos, err := rm.getAllRepos() - if err != nil { - return err - } - - var eg sync.ErrorGroup - for _, repo := range repos { - this := repo - eg.Go(func() error { - rm.log.WithField("repo", this).Debugf("Pre-prune checking repo %s", this) - if err := rm.CheckRepo(this); err != nil { - return err - } - - rm.log.WithField("repo", this).Debugf("Pruning repo %s", this) - if err := rm.PruneRepo(this); err != nil { - return err - } - - rm.log.WithField("repo", this).Debugf("Post-prune checking repo %s", this) - return rm.CheckRepo(this) - }) - } - - return kerrs.NewAggregate(eg.Wait()) + return rm.exec(InitCommand(repo.Spec.ResticIdentifier)) } func (rm *repositoryManager) CheckRepo(name string) error { + repo, err := getRepo(rm.repoLister, rm.namespace, name) + if err != nil { + return err + } + rm.repoLocker.LockExclusive(name) defer rm.repoLocker.UnlockExclusive(name) - cmd := CheckCommand(rm.config.repoPrefix, name) + cmd := CheckCommand(repo.Spec.ResticIdentifier) - return errorOnly(rm.exec(cmd)) + return rm.exec(cmd) } func (rm *repositoryManager) PruneRepo(name string) error { + repo, err := getReadyRepo(rm.repoLister, rm.namespace, name) + if err != nil { + return err + } + rm.repoLocker.LockExclusive(name) defer rm.repoLocker.UnlockExclusive(name) - cmd := PruneCommand(rm.config.repoPrefix, name) + cmd := PruneCommand(repo.Spec.ResticIdentifier) - return errorOnly(rm.exec(cmd)) + return rm.exec(cmd) } func (rm *repositoryManager) Forget(snapshot SnapshotIdentifier) error { + repo, err := getReadyRepo(rm.repoLister, rm.namespace, snapshot.Repo) + if err != nil { + return err + } + rm.repoLocker.LockExclusive(snapshot.Repo) defer rm.repoLocker.UnlockExclusive(snapshot.Repo) - cmd := ForgetCommand(rm.config.repoPrefix, snapshot.Repo, snapshot.SnapshotID) + cmd := ForgetCommand(repo.Spec.ResticIdentifier, snapshot.SnapshotID) - return errorOnly(rm.exec(cmd)) + return rm.exec(cmd) } -func (rm *repositoryManager) exec(cmd *Command) ([]byte, error) { - file, err := TempCredentialsFile(rm.secretsLister, cmd.Repo) +func (rm *repositoryManager) exec(cmd *Command) error { + file, err := TempCredentialsFile(rm.secretsLister, cmd.RepoName()) if err != nil { - return nil, err + return err } // ignore error since there's nothing we can do and it's a temp file. defer os.Remove(file) cmd.PasswordFile = file - output, err := cmd.Cmd().Output() - rm.log.WithField("repository", cmd.Repo).Debugf("Ran restic command=%q, output=%s", cmd.String(), output) + stdout, stderr, err := arkexec.RunCommand(cmd.Cmd()) + rm.log.WithFields(logrus.Fields{ + "repository": cmd.RepoName(), + "command": cmd.String(), + "stdout": stdout, + "stderr": stderr, + }).Debugf("Ran restic command") if err != nil { - if exitErr, ok := err.(*exec.ExitError); ok { - return nil, errors.Wrapf(err, "error running command, stderr=%s", exitErr.Stderr) - } - return nil, errors.Wrap(err, "error running command") + return errors.Wrapf(err, "error running command=%s, stdout=%s, stderr=%s", cmd.String(), stdout, stderr) } - return output, nil -} - -func errorOnly(_ interface{}, err error) error { - return err + return nil } diff --git a/pkg/restic/restorer.go b/pkg/restic/restorer.go index 2cc9eebb6..ebfdd3a1a 100644 --- a/pkg/restic/restorer.go +++ b/pkg/restic/restorer.go @@ -28,6 +28,7 @@ import ( "k8s.io/client-go/tools/cache" arkv1api "github.com/heptio/ark/pkg/apis/ark/v1" + arkv1listers "github.com/heptio/ark/pkg/generated/listers/ark/v1" "github.com/heptio/ark/pkg/util/boolptr" ) @@ -40,16 +41,24 @@ type Restorer interface { type restorer struct { ctx context.Context repoManager *repositoryManager + repoLister arkv1listers.ResticRepositoryLister resultsLock sync.Mutex results map[string]chan *arkv1api.PodVolumeRestore } -func newRestorer(ctx context.Context, rm *repositoryManager, podVolumeRestoreInformer cache.SharedIndexInformer) *restorer { +func newRestorer( + ctx context.Context, + rm *repositoryManager, + podVolumeRestoreInformer cache.SharedIndexInformer, + repoLister arkv1listers.ResticRepositoryLister, +) *restorer { r := &restorer{ ctx: ctx, repoManager: rm, - results: make(map[string]chan *arkv1api.PodVolumeRestore), + repoLister: repoLister, + + results: make(map[string]chan *arkv1api.PodVolumeRestore), } podVolumeRestoreInformer.AddEventHandler( @@ -76,6 +85,11 @@ func (r *restorer) RestorePodVolumes(restore *arkv1api.Restore, pod *corev1api.P return nil } + repo, err := getReadyRepo(r.repoLister, restore.Namespace, pod.Namespace) + if err != nil { + return []error{err} + } + resultsChan := make(chan *arkv1api.PodVolumeRestore) r.resultsLock.Lock() @@ -91,7 +105,7 @@ func (r *restorer) RestorePodVolumes(restore *arkv1api.Restore, pod *corev1api.P r.repoManager.repoLocker.Lock(pod.Namespace) defer r.repoManager.repoLocker.Unlock(pod.Namespace) - volumeRestore := newPodVolumeRestore(restore, pod, volume, snapshot, r.repoManager.config.repoPrefix) + volumeRestore := newPodVolumeRestore(restore, pod, volume, snapshot, repo.Spec.ResticIdentifier) if err := errorOnly(r.repoManager.arkClient.ArkV1().PodVolumeRestores(volumeRestore.Namespace).Create(volumeRestore)); err != nil { errs = append(errs, errors.WithStack(err)) @@ -120,7 +134,7 @@ ForEachVolume: return errs } -func newPodVolumeRestore(restore *arkv1api.Restore, pod *corev1api.Pod, volume, snapshot, repoPrefix string) *arkv1api.PodVolumeRestore { +func newPodVolumeRestore(restore *arkv1api.Restore, pod *corev1api.Pod, volume, snapshot, repoIdentifier string) *arkv1api.PodVolumeRestore { return &arkv1api.PodVolumeRestore{ ObjectMeta: metav1.ObjectMeta{ Namespace: restore.Namespace, @@ -147,9 +161,9 @@ func newPodVolumeRestore(restore *arkv1api.Restore, pod *corev1api.Pod, volume, Name: pod.Name, UID: pod.UID, }, - Volume: volume, - SnapshotID: snapshot, - RepoPrefix: repoPrefix, + Volume: volume, + SnapshotID: snapshot, + RepoIdentifier: repoIdentifier, }, } } diff --git a/pkg/util/exec/exec.go b/pkg/util/exec/exec.go new file mode 100644 index 000000000..4c9bb6d6e --- /dev/null +++ b/pkg/util/exec/exec.go @@ -0,0 +1,54 @@ +/* +Copyright 2018 the Heptio Ark 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 exec + +import ( + "bytes" + "io/ioutil" + "os/exec" + + "github.com/pkg/errors" +) + +// RunCommand runs a command and returns its stdout, stderr, and its returned +// error (if any). If there are errors reading stdout or stderr, their return +// value(s) will contain the error as a string. +func RunCommand(cmd *exec.Cmd) (string, string, error) { + stdoutBuf := new(bytes.Buffer) + stderrBuf := new(bytes.Buffer) + + cmd.Stdout = stdoutBuf + cmd.Stderr = stderrBuf + + runErr := cmd.Run() + + var stdout, stderr string + + if res, readErr := ioutil.ReadAll(stdoutBuf); readErr != nil { + stdout = errors.Wrap(readErr, "error reading command's stdout").Error() + } else { + stdout = string(res) + } + + if res, readErr := ioutil.ReadAll(stderrBuf); readErr != nil { + stderr = errors.Wrap(readErr, "error reading command's stderr").Error() + } else { + stderr = string(res) + } + + return stdout, stderr, runErr +}