From 02e3180cd829d932bc806aaf311c8ca808e09ec3 Mon Sep 17 00:00:00 2001 From: Dmitriy Ivolgin Date: Tue, 17 Dec 2024 08:03:20 -0800 Subject: [PATCH 1/3] Ingress will only have an IP address when type is LoadBalancer (#5047) --- pkg/appstate/ingress.go | 43 ++++++--- pkg/appstate/ingress_test.go | 165 ++++++++++++++++++++++++++++++++--- 2 files changed, 181 insertions(+), 27 deletions(-) diff --git a/pkg/appstate/ingress.go b/pkg/appstate/ingress.go index 891eab3d86..a4b2428d7b 100644 --- a/pkg/appstate/ingress.go +++ b/pkg/appstate/ingress.go @@ -7,6 +7,7 @@ import ( "github.com/replicatedhq/kots/pkg/appstate/types" "github.com/replicatedhq/kots/pkg/k8sutil" "github.com/replicatedhq/kots/pkg/logger" + v1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -118,6 +119,7 @@ func makeIngressResourceState(r *networkingv1.Ingress, state types.State) types. } func CalculateIngressState(clientset kubernetes.Interface, r *networkingv1.Ingress) types.State { + ctx := context.TODO() ns := r.Namespace backend := r.Spec.DefaultBackend @@ -139,30 +141,43 @@ func CalculateIngressState(clientset kubernetes.Interface, r *networkingv1.Ingre ns = metav1.NamespaceSystem } - var states []types.State + services := []*v1.Service{} // includes nils which are mapped to unavailable if backend != nil { - states = append(states, ingressGetStateFromBackend(clientset, ns, *backend)) + service, _ := clientset.CoreV1().Services(ns).Get(ctx, backend.Service.Name, metav1.GetOptions{}) + services = append(services, service) } for _, rules := range r.Spec.Rules { for _, path := range rules.HTTP.Paths { - states = append(states, ingressGetStateFromBackend(clientset, r.Namespace, path.Backend)) + service, _ := clientset.CoreV1().Services(r.Namespace).Get(ctx, path.Backend.Service.Name, metav1.GetOptions{}) + services = append(services, service) } } - // https://github.com/kubernetes/kubernetes/blob/badcd4af3f592376ce891b7c1b7a43ed6a18a348/pkg/printers/internalversion/printers.go#L1067 - states = append(states, ingressGetStateFromExternalIP(r)) - return types.MinState(states...) -} -func ingressGetStateFromBackend(clientset kubernetes.Interface, namespace string, backend networkingv1.IngressBackend) (minState types.State) { - if backend.Service == nil { - return types.StateUnavailable + hasLoadBalancer := false + for _, service := range services { + if service != nil && service.Spec.Type == v1.ServiceTypeLoadBalancer { + hasLoadBalancer = true + break + } } - service, _ := clientset.CoreV1().Services(namespace).Get(context.TODO(), backend.Service.Name, metav1.GetOptions{}) - if service == nil { - return types.StateUnavailable + + var states []types.State + for _, service := range services { + if service == nil { + states = append(states, types.StateUnavailable) + } else { + states = append(states, serviceGetStateFromEndpoints(clientset, service)) + } + } + + // An ingress will have an IP associated with it if it's type is LoadBalancer. + if hasLoadBalancer { + // https://github.com/kubernetes/kubernetes/blob/badcd4af3f592376ce891b7c1b7a43ed6a18a348/pkg/printers/internalversion/printers.go#L1067 + states = append(states, ingressGetStateFromExternalIP(r)) } - return serviceGetStateFromEndpoints(clientset, service) + + return types.MinState(states...) } func ingressGetStateFromExternalIP(ing *networkingv1.Ingress) types.State { diff --git a/pkg/appstate/ingress_test.go b/pkg/appstate/ingress_test.go index 17073acc17..b826fc4858 100644 --- a/pkg/appstate/ingress_test.go +++ b/pkg/appstate/ingress_test.go @@ -16,7 +16,7 @@ import ( func mockClientsetK8sVersion(expectedMajor string, expectedMinor string) kubernetes.Interface { clientset := fake.NewSimpleClientset( - // add a service + // Defaul backend service and endpoint &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "default-http-backend", @@ -52,6 +52,82 @@ func mockClientsetK8sVersion(expectedMajor string, expectedMinor string) kuberne }, }, }, + + // LoadBalancer service and endpoint + &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app-lb", + Namespace: "", + }, + Spec: v1.ServiceSpec{ + Type: v1.ServiceTypeLoadBalancer, + Ports: []v1.ServicePort{ + { + Name: "http", + Port: 8080, + }, + }, + }, + }, + &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app-lb", + Namespace: "", + }, + Subsets: []v1.EndpointSubset{ + { + Ports: []v1.EndpointPort{ + { + Name: "http", + Port: 8080, + }, + }, + Addresses: []v1.EndpointAddress{ + { + IP: "172.0.0.2", + }, + }, + }, + }, + }, + + // NodePort service and endpoint + &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app-nodeport", + Namespace: "", + }, + Spec: v1.ServiceSpec{ + Type: v1.ServiceTypeNodePort, + Ports: []v1.ServicePort{ + { + Name: "http", + Port: 8080, + }, + }, + }, + }, + &v1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app-nodeport", + Namespace: "", + }, + Subsets: []v1.EndpointSubset{ + { + Ports: []v1.EndpointPort{ + { + Name: "http", + Port: 8080, + }, + }, + Addresses: []v1.EndpointAddress{ + { + IP: "172.0.0.2", + }, + }, + }, + }, + }, ) clientset.Discovery().(*discoveryfake.FakeDiscovery).FakedServerVersion = &version.Info{ Major: expectedMajor, @@ -88,22 +164,30 @@ func TestCalculateIngressState(t *testing.T) { }, }, want: types.StateReady, - }, - { - name: "expect unavailable state when ingress with k8s version > 1.22 and no default backend", - args: args{ - clientset: mockClientsetK8sVersion("1", "23"), - r: &networkingv1.Ingress{ - Spec: networkingv1.IngressSpec{}, - }, - }, - want: types.StateUnavailable, }, { - name: "expect ready state when ingress with k8s version > 1.22 and no default backend and with load balancer status", + name: "expect ready state when there is a load balancer and an IP address", args: args{ clientset: mockClientsetK8sVersion("1", "23"), r: &networkingv1.Ingress{ - Spec: networkingv1.IngressSpec{}, + Spec: networkingv1.IngressSpec{ + Rules: []networkingv1.IngressRule{ + { + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{ + Paths: []networkingv1.HTTPIngressPath{ + { + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "app-lb", + }, + }, + }, + }, + }, + }, + }, + }, + }, Status: networkingv1.IngressStatus{ LoadBalancer: networkingv1.IngressLoadBalancerStatus{ Ingress: []networkingv1.IngressLoadBalancerIngress{ @@ -116,6 +200,61 @@ func TestCalculateIngressState(t *testing.T) { }, }, want: types.StateReady, + }, { + name: "expect ready state when there is no LoadBalancer and no address is assigned", + args: args{ + clientset: mockClientsetK8sVersion("1", "23"), + r: &networkingv1.Ingress{ + Spec: networkingv1.IngressSpec{ + Rules: []networkingv1.IngressRule{ + { + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{ + Paths: []networkingv1.HTTPIngressPath{ + { + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "app-nodeport", + }, + }, + }, + }, + }, + }, + }, + }, + }, + Status: networkingv1.IngressStatus{}, + }, + }, + want: types.StateReady, + }, { + name: "expect unavailable state when there is a LoadBalancer but no address is assigned", + args: args{ + clientset: mockClientsetK8sVersion("1", "23"), + r: &networkingv1.Ingress{ + Spec: networkingv1.IngressSpec{ + Rules: []networkingv1.IngressRule{ + { + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{ + Paths: []networkingv1.HTTPIngressPath{ + { + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: "app-lb", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + want: types.StateUnavailable, }, } for _, tt := range tests { From ea574d82fb8702318ef2c8c96831a474675237e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 11:57:30 -0600 Subject: [PATCH 2/3] chore(deps): bump golang.org/x/crypto in the go_modules group (#5054) Bumps the go_modules group with 1 update: [golang.org/x/crypto](https://github.com/golang/crypto). Updates `golang.org/x/crypto` from 0.28.0 to 0.31.0 - [Commits](https://github.com/golang/crypto/compare/v0.28.0...v0.31.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production dependency-group: go_modules ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Alex Parker <7272359+ajp-io@users.noreply.github.com> --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 92650a1696..f47f65a491 100644 --- a/go.mod +++ b/go.mod @@ -68,9 +68,9 @@ require ( github.com/vmware-tanzu/velero v1.14.1 go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.28.0 + golang.org/x/crypto v0.31.0 golang.org/x/oauth2 v0.23.0 - golang.org/x/sync v0.8.0 + golang.org/x/sync v0.10.0 google.golang.org/api v0.200.0 gopkg.in/go-playground/assert.v1 v1.2.1 gopkg.in/ini.v1 v1.67.0 @@ -366,9 +366,9 @@ require ( golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/mod v0.21.0 // indirect golang.org/x/net v0.30.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/term v0.25.0 // indirect - golang.org/x/text v0.19.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/term v0.27.0 // indirect + golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.7.0 // indirect golang.org/x/tools v0.24.0 // indirect google.golang.org/genproto v0.0.0-20241007155032-5fefd90f89a9 // indirect diff --git a/go.sum b/go.sum index 7c18ea3b9a..aefdc7fe8b 100644 --- a/go.sum +++ b/go.sum @@ -1700,8 +1700,8 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58 golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1860,8 +1860,8 @@ golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180117170059-2c42eef0765b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1967,8 +1967,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1978,8 +1978,8 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= -golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1997,8 +1997,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= From eda1bc003bf9dc68e02c96b2cb9be06df1ee7ba7 Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Tue, 17 Dec 2024 14:17:00 -0800 Subject: [PATCH 3/3] feat(snapshots): GetBackup details endpoint should support new dr (#5058) * feat(snapshots): GetBackup details endpoint should support new dr * backwards compatibility * tests * remove old response field * pin apko-publish action * pin apko-publish action * pin apko-publish action * pin apko-publish action * pin apko-publish action --- .../build-custom-image-with-apko/action.yml | 4 +- .../build-dep-image-with-apko/action.yml | 4 +- pkg/handlers/backup.go | 10 +- pkg/kotsadmsnapshot/backup.go | 51 ++++- pkg/kotsadmsnapshot/backup_test.go | 188 ++++++++++++++++++ pkg/kotsadmsnapshot/types/types.go | 4 + web/src/components/RestoreSnapshotRow.jsx | 2 +- .../components/snapshots/SnapshotDetails.jsx | 2 +- 8 files changed, 250 insertions(+), 15 deletions(-) diff --git a/.github/actions/build-custom-image-with-apko/action.yml b/.github/actions/build-custom-image-with-apko/action.yml index d31e09cd5f..3c8eb47947 100644 --- a/.github/actions/build-custom-image-with-apko/action.yml +++ b/.github/actions/build-custom-image-with-apko/action.yml @@ -47,8 +47,10 @@ runs: export GIT_TAG=${{ inputs.git-tag }} envsubst '${GIT_TAG}' < ${{ inputs.context }}/apko.yaml.tmpl > ${{ inputs.context }}/apko.yaml - - uses: chainguard-images/actions/apko-publish@main + # pin to work around https://github.com/chainguard-images/actions/issues/160 + - uses: chainguard-images/actions/apko-publish@49e3bc2feb790da6c3a7f749b38c769174c4ad54 with: + apko-image: ghcr.io/wolfi-dev/sdk@sha256:b3c690e2aff7a6e6259632047b5d6133169204f7011b9936731cf3f403d5a8c1 config: ${{ inputs.context }}/apko.yaml archs: amd64,arm64 tag: ${{ inputs.image-name }} diff --git a/.github/actions/build-dep-image-with-apko/action.yml b/.github/actions/build-dep-image-with-apko/action.yml index 7f234ca742..b10126806f 100644 --- a/.github/actions/build-dep-image-with-apko/action.yml +++ b/.github/actions/build-dep-image-with-apko/action.yml @@ -40,9 +40,11 @@ runs: echo "image-exists=false" >> "$GITHUB_OUTPUT" fi - - uses: chainguard-images/actions/apko-publish@main + # pin to work around https://github.com/chainguard-images/actions/issues/160 + - uses: chainguard-images/actions/apko-publish@49e3bc2feb790da6c3a7f749b38c769174c4ad54 if: ${{ inputs.overwrite == 'true' || steps.check-image-exists.outputs.image-exists == 'false' }} with: + apko-image: ghcr.io/wolfi-dev/sdk@sha256:b3c690e2aff7a6e6259632047b5d6133169204f7011b9936731cf3f403d5a8c1 config: ${{ inputs.apko-config }} archs: amd64,arm64 tag: ${{ inputs.image-name }} diff --git a/pkg/handlers/backup.go b/pkg/handlers/backup.go index 26ca52925d..8a39ae2976 100644 --- a/pkg/handlers/backup.go +++ b/pkg/handlers/backup.go @@ -130,22 +130,22 @@ func (h *Handler) ListInstanceBackups(w http.ResponseWriter, r *http.Request) { } type GetBackupResponse struct { - BackupDetail *snapshottypes.BackupDetail `json:"backupDetail"` - Success bool `json:"success"` - Error string `json:"error,omitempty"` + BackupDetails []snapshottypes.BackupDetail `json:"backupDetails"` + Success bool `json:"success"` + Error string `json:"error,omitempty"` } func (h *Handler) GetBackup(w http.ResponseWriter, r *http.Request) { getBackupResponse := GetBackupResponse{} - backup, err := snapshot.GetBackupDetail(r.Context(), util.PodNamespace, mux.Vars(r)["snapshotName"]) + backups, err := snapshot.GetBackupDetail(r.Context(), util.PodNamespace, mux.Vars(r)["snapshotName"]) if err != nil { logger.Error(err) getBackupResponse.Error = "failed to get backup detail" JSON(w, 500, getBackupResponse) return } - getBackupResponse.BackupDetail = backup + getBackupResponse.BackupDetails = backups getBackupResponse.Success = true diff --git a/pkg/kotsadmsnapshot/backup.go b/pkg/kotsadmsnapshot/backup.go index de7497a757..ed6cef89cb 100644 --- a/pkg/kotsadmsnapshot/backup.go +++ b/pkg/kotsadmsnapshot/backup.go @@ -1102,7 +1102,7 @@ func HasUnfinishedInstanceBackup(ctx context.Context, kotsadmNamespace string) ( return false, nil } -func GetBackupDetail(ctx context.Context, kotsadmNamespace string, backupID string) (*types.BackupDetail, error) { +func GetBackupDetail(ctx context.Context, kotsadmNamespace string, backupName string) ([]types.BackupDetail, error) { cfg, err := k8sutil.GetClusterConfig() if err != nil { return nil, errors.Wrap(err, "failed to get cluster config") @@ -1125,20 +1125,59 @@ func GetBackupDetail(ctx context.Context, kotsadmNamespace string, backupID stri veleroNamespace := backendStorageLocation.Namespace - backup, err := veleroClient.Backups(veleroNamespace).Get(ctx, backupID, metav1.GetOptions{}) + backups, err := listBackupsByName(ctx, veleroClient, veleroNamespace, backupName) if err != nil { return nil, errors.Wrap(err, "failed to get backup") } + results := []types.BackupDetail{} + + for _, backup := range backups { + result, err := getBackupDetailForBackup(ctx, veleroClient, veleroNamespace, backup) + if err != nil { + return nil, fmt.Errorf("failed to get backup detail for backup %s: %w", backup.Name, err) + } + + results = append(results, *result) + } + + return results, nil +} + +// listBackupsByName returns a list of backups for the specified backup name. First it tries to get +// the backup by the replicated.com/backup-name label, and if that fails, it tries to get the +// backup by the metadata name. +func listBackupsByName(ctx context.Context, veleroClient veleroclientv1.VeleroV1Interface, veleroNamespace string, backupName string) ([]velerov1.Backup, error) { + // first try to get the backup from the backup-name label + backupList, err := veleroClient.Backups(veleroNamespace).List(ctx, metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s", types.InstanceBackupNameLabel, velerolabel.GetValidName(backupName)), + }) + if err != nil { + return nil, fmt.Errorf("failed to list backups by label: %w", err) + } + if len(backupList.Items) > 0 { + return backupList.Items, nil + } + backup, err := veleroClient.Backups(veleroNamespace).Get(ctx, backupName, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to get backup by name: %w", err) + } + + return []velerov1.Backup{*backup}, nil +} + +// getBackupDetailForBackup returns a BackupDetail object for the specified backup. +func getBackupDetailForBackup(ctx context.Context, veleroClient veleroclientv1.VeleroV1Interface, veleroNamespace string, backup velerov1.Backup) (*types.BackupDetail, error) { backupVolumes, err := veleroClient.PodVolumeBackups(veleroNamespace).List(ctx, metav1.ListOptions{ - LabelSelector: fmt.Sprintf("velero.io/backup-name=%s", velerolabel.GetValidName(backupID)), + LabelSelector: fmt.Sprintf("velero.io/backup-name=%s", velerolabel.GetValidName(backup.Name)), }) if err != nil { return nil, errors.Wrap(err, "failed to list volumes") } - result := &types.BackupDetail{ + result := types.BackupDetail{ Name: backup.Name, + Type: types.GetInstanceBackupType(backup), Status: string(backup.Status.Phase), Namespaces: backup.Spec.IncludedNamespaces, Volumes: listBackupVolumes(backupVolumes.Items), @@ -1151,7 +1190,7 @@ func GetBackupDetail(ctx context.Context, kotsadmNamespace string, backupID stri result.VolumeSizeHuman = units.HumanSize(float64(totalBytesDone)) // TODO: should this be TotalBytes rather than BytesDone? if backup.Status.Phase == velerov1.BackupPhaseCompleted || backup.Status.Phase == velerov1.BackupPhasePartiallyFailed || backup.Status.Phase == velerov1.BackupPhaseFailed { - errs, warnings, execs, err := downloadBackupLogs(ctx, veleroNamespace, backupID) + errs, warnings, execs, err := downloadBackupLogs(ctx, veleroNamespace, backup.Name) result.Errors = errs result.Warnings = warnings result.Hooks = execs @@ -1161,7 +1200,7 @@ func GetBackupDetail(ctx context.Context, kotsadmNamespace string, backupID stri } } - return result, nil + return &result, nil } func listBackupVolumes(backupVolumes []velerov1.PodVolumeBackup) []types.SnapshotVolume { diff --git a/pkg/kotsadmsnapshot/backup_test.go b/pkg/kotsadmsnapshot/backup_test.go index 3475f9e892..d81fd78cc5 100644 --- a/pkg/kotsadmsnapshot/backup_test.go +++ b/pkg/kotsadmsnapshot/backup_test.go @@ -3959,3 +3959,191 @@ func TestDeleteBackup(t *testing.T) { }) } } + +func TestGetBackupDetail(t *testing.T) { + scheme := runtime.NewScheme() + corev1.AddToScheme(scheme) + velerov1.AddToScheme(scheme) + + objects := []runtime.Object{ + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kotsadm-velero-namespace", + }, + Data: map[string]string{ + "veleroNamespace": "velero", + }, + }, + &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "velero", + Namespace: "velero", + }, + }, + } + + veleroObjects := []runtime.Object{ + &velerov1.BackupStorageLocation{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + Namespace: "velero", + }, + Spec: velerov1.BackupStorageLocationSpec{ + Provider: "aws", + Default: true, + }, + }, + &velerov1.Backup{ + TypeMeta: metav1.TypeMeta{ + Kind: "Backup", + APIVersion: "velero.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "instance-abcd", + Namespace: "velero", + Labels: map[string]string{ + types.InstanceBackupNameLabel: "app-slug-abcd", + }, + Annotations: map[string]string{ + types.BackupIsECAnnotation: "true", + types.InstanceBackupVersionAnnotation: types.InstanceBackupVersionCurrent, + types.InstanceBackupTypeAnnotation: types.InstanceBackupTypeInfra, + types.InstanceBackupCountAnnotation: "2", + }, + CreationTimestamp: metav1.Time{Time: time.Date(2022, 1, 3, 0, 0, 0, 0, time.Local)}, + }, + Spec: velerov1.BackupSpec{ + StorageLocation: "default", + IncludedNamespaces: []string{"*"}, + }, + Status: velerov1.BackupStatus{ + Phase: velerov1.BackupPhaseInProgress, + }, + }, + &velerov1.Backup{ + TypeMeta: metav1.TypeMeta{ + Kind: "Backup", + APIVersion: "velero.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "application-abcd", + Namespace: "velero", + Labels: map[string]string{ + types.InstanceBackupNameLabel: "app-slug-abcd", + }, + Annotations: map[string]string{ + types.BackupIsECAnnotation: "true", + types.InstanceBackupVersionAnnotation: types.InstanceBackupVersionCurrent, + types.InstanceBackupTypeAnnotation: types.InstanceBackupTypeApp, + types.InstanceBackupCountAnnotation: "2", + }, + CreationTimestamp: metav1.Time{Time: time.Date(2022, 1, 4, 0, 0, 0, 0, time.Local)}, + }, + Spec: velerov1.BackupSpec{ + StorageLocation: "default", + IncludedNamespaces: []string{"*"}, + }, + Status: velerov1.BackupStatus{ + Phase: velerov1.BackupPhaseInProgress, + }, + }, + &velerov1.Backup{ + TypeMeta: metav1.TypeMeta{ + Kind: "Backup", + APIVersion: "velero.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "instance-efgh", + Namespace: "velero", + Annotations: map[string]string{ + types.BackupIsECAnnotation: "true", + types.InstanceBackupAnnotation: "true", + // legacy backups do not have the InstanceBackupTypeAnnotation + }, + CreationTimestamp: metav1.Time{Time: time.Date(2022, 1, 2, 0, 0, 0, 0, time.Local)}, + }, + Spec: velerov1.BackupSpec{ + StorageLocation: "default", + IncludedNamespaces: []string{"*"}, + }, + Status: velerov1.BackupStatus{ + Phase: velerov1.BackupPhaseInProgress, + }, + }, + } + + type args struct { + backupName string + } + tests := []struct { + name string + args args + want []types.BackupDetail + wantErr bool + }{ + { + name: "legacy backup by name should return a single backup from metadata.name", + args: args{ + backupName: "instance-efgh", + }, + want: []types.BackupDetail{ + { + Name: "instance-efgh", + Type: types.InstanceBackupTypeLegacy, + Status: "InProgress", + Namespaces: []string{"*"}, + VolumeSizeHuman: "0B", + Volumes: []types.SnapshotVolume{}, + }, + }, + }, + { + name: "new backup by name label should return multiple backups", + args: args{ + backupName: "app-slug-abcd", + }, + want: []types.BackupDetail{ + { + Name: "application-abcd", + Type: types.InstanceBackupTypeApp, + Status: "InProgress", + Namespaces: []string{"*"}, + VolumeSizeHuman: "0B", + Volumes: []types.SnapshotVolume{}, + }, + { + Name: "instance-abcd", + Type: types.InstanceBackupTypeInfra, + Status: "InProgress", + Namespaces: []string{"*"}, + VolumeSizeHuman: "0B", + Volumes: []types.SnapshotVolume{}, + }, + }, + }, + { + name: "not found should return an error", + args: args{ + backupName: "not-exists", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k8sclient.SetBuilder(&k8sclient.MockBuilder{ + Client: fake.NewSimpleClientset(objects...), + }) + veleroclient.SetBuilder(&veleroclient.MockBuilder{ + Client: velerofake.NewSimpleClientset(veleroObjects...).VeleroV1(), + }) + got, err := GetBackupDetail(context.Background(), "velero", tt.args.backupName) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/kotsadmsnapshot/types/types.go b/pkg/kotsadmsnapshot/types/types.go index 23b339e5e6..d07e888c58 100644 --- a/pkg/kotsadmsnapshot/types/types.go +++ b/pkg/kotsadmsnapshot/types/types.go @@ -8,6 +8,9 @@ import ( ) const ( + // BackupIsECAnnotation is the annotation used to store if the backup is from an EC install. + BackupIsECAnnotation = "kots.io/embedded-cluster" + // InstanceBackupNameLabel is the label used to store the name of the backup for an instance // backup. InstanceBackupNameLabel = "replicated.com/backup-name" @@ -83,6 +86,7 @@ type Backup struct { type BackupDetail struct { Name string `json:"name"` + Type string `json:"type"` Status string `json:"status"` VolumeSizeHuman string `json:"volumeSizeHuman"` Namespaces []string `json:"namespaces"` diff --git a/web/src/components/RestoreSnapshotRow.jsx b/web/src/components/RestoreSnapshotRow.jsx index 845f6bc5c6..8177440350 100644 --- a/web/src/components/RestoreSnapshotRow.jsx +++ b/web/src/components/RestoreSnapshotRow.jsx @@ -32,7 +32,7 @@ class RestoreSnapshotRow extends Component { if (result.success) { this.setState({ isLoadingBackupInfo: false, - backupInfo: result.backupDetail, + backupInfo: result.backupDetails?.[0], backupInfoMsg: "", }); } else { diff --git a/web/src/components/snapshots/SnapshotDetails.jsx b/web/src/components/snapshots/SnapshotDetails.jsx index 96c922242c..cc30f34029 100644 --- a/web/src/components/snapshots/SnapshotDetails.jsx +++ b/web/src/components/snapshots/SnapshotDetails.jsx @@ -158,7 +158,7 @@ class SnapshotDetails extends Component { } const response = await res.json(); - const snapshotDetails = response.backupDetail; + const snapshotDetails = response.backupDetails?.[0]; let series = []; if (!isEmpty(snapshotDetails?.volumes)) {