From 8338617fde6e3c138a7878c71da847f8b2e15ad9 Mon Sep 17 00:00:00 2001 From: Salah Al Saleh Date: Thu, 19 Dec 2024 14:00:31 -0800 Subject: [PATCH] EC v2 to v2 upgrades (#5060) * EC v2 to v2 upgrades --- cmd/imagedeps/tag-finder.go | 4 +- dev/scripts/common.sh | 6 +- dev/scripts/up-ec.sh | 2 +- go.mod | 50 +-- go.sum | 97 ++--- migrations/tables/plan.yaml | 37 ++ pkg/apiserver/server.go | 5 + pkg/embeddedcluster/monitor.go | 2 +- pkg/embeddedcluster/util.go | 2 +- pkg/embeddedcluster/util_test.go | 6 +- pkg/handlers/deploy_ec2.go | 244 +++++++++++ pkg/handlers/handlers.go | 10 + pkg/handlers/handlers_test.go | 24 ++ pkg/handlers/interface.go | 5 + pkg/handlers/metadata.go | 3 + pkg/handlers/mock/mock.go | 36 ++ pkg/kotsadmsnapshot/backup_test.go | 1 + pkg/kotsutil/kots.go | 14 + pkg/plan/app.go | 217 ++++++++++ pkg/plan/cluster.go | 116 ++++++ pkg/plan/extensions.go | 104 +++++ pkg/plan/plan.go | 382 ++++++++++++++++++ pkg/plan/types/types.go | 118 ++++++ pkg/plan/util.go | 192 +++++++++ pkg/replicatedapp/api_test.go | 2 +- pkg/replicatedapp/upstream.go | 4 +- pkg/store/kotsstore/plan_store.go | 103 +++++ pkg/store/mock/mock.go | 242 ++++++++--- pkg/store/store_interface.go | 8 + pkg/upgradeservice/bootstrap.go | 12 +- pkg/upgradeservice/deploy/deploy_ec2.go | 99 +++++ pkg/upgradeservice/handlers/deploy_ec2.go | 89 ++++ pkg/upgradeservice/handlers/handlers.go | 1 + pkg/upgradeservice/handlers/info.go | 3 + pkg/upgradeservice/handlers/interface.go | 1 + pkg/upgradeservice/plan/plan.go | 44 ++ pkg/upgradeservice/types/types.go | 61 +-- pkg/upstream/replicated.go | 2 +- pkg/util/util.go | 4 + pkg/websocket/upgrade.go | 128 ++++++ web/src/Root.tsx | 48 ++- web/src/components/apps/AppVersionHistory.tsx | 42 +- .../components/modals/UpgradeStatusModal.tsx | 17 +- .../upgrade_service/ConfirmAndDeploy.tsx | 4 +- .../upgrade_service/UpgradeService.tsx | 1 + .../upgrade_service/hooks/getUpgradeInfo.tsx | 1 + .../hooks/postDeployAppVersion.tsx | 36 +- web/src/types/index.ts | 1 + 48 files changed, 2398 insertions(+), 232 deletions(-) create mode 100644 migrations/tables/plan.yaml create mode 100644 pkg/handlers/deploy_ec2.go create mode 100644 pkg/plan/app.go create mode 100644 pkg/plan/cluster.go create mode 100644 pkg/plan/extensions.go create mode 100644 pkg/plan/plan.go create mode 100644 pkg/plan/types/types.go create mode 100644 pkg/plan/util.go create mode 100644 pkg/store/kotsstore/plan_store.go create mode 100644 pkg/upgradeservice/deploy/deploy_ec2.go create mode 100644 pkg/upgradeservice/handlers/deploy_ec2.go create mode 100644 pkg/upgradeservice/plan/plan.go create mode 100644 pkg/websocket/upgrade.go diff --git a/cmd/imagedeps/tag-finder.go b/cmd/imagedeps/tag-finder.go index 84f3c62d98..e883d3aee9 100644 --- a/cmd/imagedeps/tag-finder.go +++ b/cmd/imagedeps/tag-finder.go @@ -10,7 +10,7 @@ import ( "sort" "strings" - semver "github.com/Masterminds/semver/v3" + "github.com/Masterminds/semver" "github.com/google/go-github/v39/github" "github.com/heroku/docker-registry-client/registry" "golang.org/x/oauth2" @@ -183,7 +183,7 @@ func getLatestTagFromRegistry(imageUri string, getTags getTagsFn, match filterFn if match(tag) { v, err := semver.NewVersion(tag) if err != nil { - return "", err + return "", fmt.Errorf("parse semver %q: %w", tag, err) } versions = append(versions, v) } diff --git a/dev/scripts/common.sh b/dev/scripts/common.sh index 77649b2fe3..bb16033b92 100644 --- a/dev/scripts/common.sh +++ b/dev/scripts/common.sh @@ -94,8 +94,10 @@ function ec_patch() { } function ec_build_and_load() { + force=$2 + # Build the image - if docker images | grep -q "$(image $1)"; then + if [ -z "$force" ] && docker images | grep -q "$(image $1)"; then echo "$(image $1) image already exists, skipping build..." else echo "Building $1..." @@ -104,7 +106,7 @@ function ec_build_and_load() { fi # Load the image into the embedded cluster - if docker exec $(ec_node) k0s ctr images ls | grep -q "$(image $1)"; then + if [ -z "$force" ] && docker exec $(ec_node) k0s ctr images ls | grep -q "$(image $1)"; then echo "$(image $1) image already loaded in embedded cluster, skipping import..." else echo "Loading "$(image $1)" image into embedded cluster..." diff --git a/dev/scripts/up-ec.sh b/dev/scripts/up-ec.sh index 630a7b566e..dd6f639691 100755 --- a/dev/scripts/up-ec.sh +++ b/dev/scripts/up-ec.sh @@ -23,7 +23,7 @@ ec_build_and_load "$component" # Use the dev image for kotsadm migrations if [ "$component" == "kotsadm" ]; then - ec_build_and_load "kotsadm-migrations" + ec_build_and_load "kotsadm-migrations" true fi # The kotsadm dev image does not have a web component, and kotsadm-web service does not exist in embedded cluster. diff --git a/go.mod b/go.mod index f47f65a491..2dc4e8a792 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/replicatedhq/kots -go 1.23.0 +go 1.23.2 + +toolchain go1.23.4 require ( cloud.google.com/go/storage v1.45.0 @@ -8,7 +10,7 @@ require ( github.com/Azure/go-autorest/autorest v0.11.29 github.com/Azure/go-autorest/autorest/adal v0.9.24 github.com/Masterminds/semver v1.5.0 - github.com/Masterminds/semver/v3 v3.3.0 + github.com/Masterminds/semver/v3 v3.3.1 github.com/Masterminds/sprig/v3 v3.3.0 github.com/ahmetalpbalkan/go-cursor v0.0.0-20131010032410-8136607ea412 github.com/aws/aws-sdk-go v1.55.5 @@ -39,8 +41,8 @@ require ( github.com/mholt/archiver/v3 v3.5.1 github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a github.com/mitchellh/hashstructure v1.1.0 - github.com/onsi/ginkgo/v2 v2.20.2 - github.com/onsi/gomega v1.34.2 + github.com/onsi/ginkgo/v2 v2.22.0 + github.com/onsi/gomega v1.36.1 github.com/open-policy-agent/opa v0.68.0 github.com/opencontainers/image-spec v1.1.0 github.com/ory/dockertest/v3 v3.10.0 @@ -49,7 +51,7 @@ require ( github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 github.com/pkg/errors v0.9.1 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 - github.com/replicatedhq/embedded-cluster/kinds v1.15.0 + github.com/replicatedhq/embedded-cluster/kinds v1.15.1-0.20241217212717-16e5694e39aa github.com/replicatedhq/kotskinds v0.0.0-20240718194123-1018dd404e95 github.com/replicatedhq/kurlkinds v1.5.0 github.com/replicatedhq/troubleshoot v0.107.5 @@ -63,7 +65,7 @@ require ( github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.19.0 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 github.com/tj/go-spin v1.1.0 github.com/vmware-tanzu/velero v1.14.1 go.uber.org/multierr v1.11.0 @@ -76,19 +78,19 @@ require ( gopkg.in/ini.v1 v1.67.0 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 - helm.sh/helm/v3 v3.16.2 - k8s.io/api v0.31.2 - k8s.io/apimachinery v0.31.2 + helm.sh/helm/v3 v3.16.3 + k8s.io/api v0.31.3 + k8s.io/apimachinery v0.31.3 k8s.io/cli-runtime v0.31.2 - k8s.io/client-go v0.31.2 + k8s.io/client-go v0.31.3 k8s.io/cluster-bootstrap v0.31.2 k8s.io/helm v2.17.0+incompatible k8s.io/kubelet v0.31.2 k8s.io/metrics v0.31.2 - k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 + k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 oras.land/oras-go/v2 v2.5.0 sigs.k8s.io/application v0.8.3 - sigs.k8s.io/controller-runtime v0.19.1 + sigs.k8s.io/controller-runtime v0.19.3 sigs.k8s.io/kustomize/api v0.18.0 sigs.k8s.io/kustomize/kyaml v0.18.1 sigs.k8s.io/yaml v1.4.0 @@ -148,16 +150,16 @@ require ( github.com/chzyer/readline v1.5.1 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/containerd/cgroups/v3 v3.0.3 // indirect - github.com/containerd/containerd v1.7.21 // indirect + github.com/containerd/containerd v1.7.23 // indirect github.com/containerd/continuity v0.4.2 // indirect - github.com/containerd/errdefs v0.1.0 // indirect + github.com/containerd/errdefs v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect github.com/containers/ocicrypt v1.2.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f // indirect - github.com/cyphar/filepath-securejoin v0.3.1 // indirect + github.com/cyphar/filepath-securejoin v0.3.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/docker/cli v27.1.1+incompatible // indirect @@ -214,7 +216,7 @@ require ( github.com/google/go-intervals v0.0.2 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 // indirect + github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect github.com/google/s2a-go v0.1.8 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect @@ -249,7 +251,7 @@ require ( github.com/jmoiron/sqlx v1.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/k0sproject/k0s v1.29.9-0.20240821114611-d76eb6bb05a7 // indirect + github.com/k0sproject/k0s v1.30.7-0.20241029184556-a942e759e13b github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/pgzip v1.2.6 // indirect @@ -364,13 +366,13 @@ require ( go.opentelemetry.io/otel/trace v1.31.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect 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/mod v0.22.0 // indirect + golang.org/x/net v0.31.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 + golang.org/x/tools v0.26.0 // indirect google.golang.org/genproto v0.0.0-20241007155032-5fefd90f89a9 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240930140551-af27646dc61f // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect @@ -379,9 +381,9 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - k8s.io/apiextensions-apiserver v0.31.2 // indirect - k8s.io/apiserver v0.31.2 // indirect - k8s.io/component-base v0.31.2 // indirect + k8s.io/apiextensions-apiserver v0.31.3 // indirect + k8s.io/apiserver v0.31.3 // indirect + k8s.io/component-base v0.31.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20240709000822-3c01b740850f // indirect k8s.io/kubectl v0.31.1 // indirect @@ -422,6 +424,8 @@ require ( github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/tchap/go-patricia/v2 v2.3.1 // indirect + github.com/vishvananda/netlink v1.3.0 // indirect + github.com/vishvananda/netns v0.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.54.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.29.0 // indirect diff --git a/go.sum b/go.sum index aefdc7fe8b..19d3e579a6 100644 --- a/go.sum +++ b/go.sum @@ -267,8 +267,8 @@ github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy86 github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= -github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= +github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= @@ -431,12 +431,12 @@ github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMe github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/containerd/cgroups/v3 v3.0.3 h1:S5ByHZ/h9PMe5IOQoN7E+nMc2UcLEM/V48DGDJ9kip0= github.com/containerd/cgroups/v3 v3.0.3/go.mod h1:8HBe7V3aWGLFPd/k03swSIsGjZhHI2WzJmticMgVuz0= -github.com/containerd/containerd v1.7.21 h1:USGXRK1eOC/SX0L195YgxTHb0a00anxajOzgfN0qrCA= -github.com/containerd/containerd v1.7.21/go.mod h1:e3Jz1rYRUZ2Lt51YrH9Rz0zPyJBOlSvB3ghr2jbVD8g= +github.com/containerd/containerd v1.7.23 h1:H2CClyUkmpKAGlhQp95g2WXHfLYc7whAuvZGBNYOOwQ= +github.com/containerd/containerd v1.7.23/go.mod h1:7QUzfURqZWCZV7RLNEn1XjUCQLEf0bkaK4GjUaZehxw= github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM= github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= -github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM= -github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0= +github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= +github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= @@ -476,8 +476,8 @@ github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f h1:eHnXnuK47UlSTOQexbzxAZfekVz6i+LKRdj1CU5DPaM= github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= -github.com/cyphar/filepath-securejoin v0.3.1 h1:1V7cHiaW+C+39wEfpH6XlLBQo3j/PciWFrgfCLS8XrE= -github.com/cyphar/filepath-securejoin v0.3.1/go.mod h1:F7i41x/9cBF7lzCrVsYs9fuzwRZm4NQsGTBdpp6mETc= +github.com/cyphar/filepath-securejoin v0.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8= +github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM= github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -869,8 +869,8 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= -github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= @@ -1069,8 +1069,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/k0sproject/k0s v1.29.9-0.20240821114611-d76eb6bb05a7 h1:pxtk/512ibsry6nNx8EfBhFOeeJ/gad1Igjr+7Th6A0= -github.com/k0sproject/k0s v1.29.9-0.20240821114611-d76eb6bb05a7/go.mod h1:eAO/EjCQxfHGnfxOUs061GdLYISLLYbXdexOPMn048g= +github.com/k0sproject/k0s v1.30.7-0.20241029184556-a942e759e13b h1:pE04QpLnpve7c+bOn4GmKu/j3IMhVBAAjvK6z7mW+FM= +github.com/k0sproject/k0s v1.30.7-0.20241029184556-a942e759e13b/go.mod h1:j0xCfV9a+hwiOOr3HJmZb1enwXz8cdkSGDzDRqKSdG0= github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= @@ -1268,15 +1268,15 @@ github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+ github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= -github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= +github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= +github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.3.0/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= -github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= +github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= +github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/open-policy-agent/opa v0.68.0 h1:Jl3U2vXRjwk7JrHmS19U3HZO5qxQRinQbJ2eCJYSqJQ= github.com/open-policy-agent/opa v0.68.0/go.mod h1:5E5SvaPwTpwt2WM177I9Z3eT7qUpmOGjk1ZdHs+TZ4w= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= @@ -1369,8 +1369,8 @@ github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDO github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= -github.com/replicatedhq/embedded-cluster/kinds v1.15.0 h1:hqlDSjyZ+kBnyPaE9GSjZN2TVZKgNc8b9tF2XgXcR8w= -github.com/replicatedhq/embedded-cluster/kinds v1.15.0/go.mod h1:eCRG7AjsENoi62kguGWKbYWMuri8uR+QV1yarvn/MBY= +github.com/replicatedhq/embedded-cluster/kinds v1.15.1-0.20241217212717-16e5694e39aa h1:fXPI/I8F2goWhpGrmJops8SW5y6RnBJ9tZGdw+Qd+x0= +github.com/replicatedhq/embedded-cluster/kinds v1.15.1-0.20241217212717-16e5694e39aa/go.mod h1:+M1m6F45l4m7z8L6sBe+iLg/IVDZkR7jEHDgM8WwEfE= github.com/replicatedhq/kotskinds v0.0.0-20240718194123-1018dd404e95 h1:JhwPz4Bgbz5iYl3UV2EB+HnF9oW/eCRi+hASAz+J6XI= github.com/replicatedhq/kotskinds v0.0.0-20240718194123-1018dd404e95/go.mod h1:QjhIUu3+OmHZ09u09j3FCoTt8F3BYtQglS+OLmftu9I= github.com/replicatedhq/kurlkinds v1.5.0 h1:zZ0PKNeh4kXvSzVGkn62DKTo314GxhXg1TSB3azURMc= @@ -1393,8 +1393,8 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rqlite/gorqlite v0.0.0-20221028154453-256f31831ff3 h1:lkui3fRWN9cUFDrsT0ym2fojX+W9y8W/9DtXSquzWlQ= github.com/rqlite/gorqlite v0.0.0-20221028154453-256f31831ff3/go.mod h1:xF/KoXmrRyahPfo5L7Szb5cAAUl53dMWBh9cMruGEZg= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= @@ -1506,8 +1506,9 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/sylabs/sif/v2 v2.18.0 h1:eXugsS1qx7St2Wu/AJ21KnsQiVCpouPlTigABh+6KYI= @@ -1545,6 +1546,11 @@ github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinC github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk= github.com/vbauerster/mpb/v8 v8.7.5 h1:hUF3zaNsuaBBwzEFoCvfuX3cpesQXZC0Phm/JcHZQ+c= github.com/vbauerster/mpb/v8 v8.7.5/go.mod h1:bRCnR7K+mj5WXKsy0NWB6Or+wctYGvVwKn6huwvxKa0= +github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk= +github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= +github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/vmware-tanzu/velero v1.10.3 h1:prE24sOog/jhBt7LivY+k6NbKwU+0nl1JAbJWA91mdo= github.com/vmware-tanzu/velero v1.10.3/go.mod h1:3/m52SOiTP/L+Qp0E6+Gk688niyVxowLCLniQbTkSPA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -1743,8 +1749,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180112015858-5ccada7d0a7b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1815,8 +1821,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1965,6 +1971,7 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.10.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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= @@ -2084,8 +2091,8 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -2375,8 +2382,8 @@ gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= -helm.sh/helm/v3 v3.16.2 h1:Y9v7ry+ubQmi+cb5zw1Llx8OKHU9Hk9NQ/+P+LGBe2o= -helm.sh/helm/v3 v3.16.2/go.mod h1:SyTXgKBjNqi2NPsHCW5dDAsHqvGIu0kdNYNH9gQaw70= +helm.sh/helm/v3 v3.16.3 h1:kb8bSxMeRJ+knsK/ovvlaVPfdis0X3/ZhYCSFRP+YmY= +helm.sh/helm/v3 v3.16.3/go.mod h1:zeVWGDR4JJgiRbT3AnNsjYaX8OTJlIE9zC+Q7F7iUSU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -2387,28 +2394,28 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 k8s.io/api v0.0.0-20190918155943-95b840bb6a1f/go.mod h1:uWuOHnjmNrtQomJrvEBg0c0HRNyQ+8KTEERVsK0PW48= k8s.io/api v0.17.0/go.mod h1:npsyOePkeP0CPwyGfXDHxvypiYMJxBWAMpQxCaJ4ZxI= k8s.io/api v0.19.0/go.mod h1:I1K45XlvTrDjmj5LoM5LuP/KYrhWbjUKT/SoPG0qTjw= -k8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0= -k8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk= +k8s.io/api v0.31.3 h1:umzm5o8lFbdN/hIXbrK9oRpOproJO62CV1zqxXrLgk8= +k8s.io/api v0.31.3/go.mod h1:UJrkIp9pnMOI9K2nlL6vwpxRzzEX5sWgn8kGQe92kCE= k8s.io/apiextensions-apiserver v0.0.0-20190918161926-8f644eb6e783/go.mod h1:xvae1SZB3E17UpV59AWc271W/Ph25N+bjPyR63X6tPY= k8s.io/apiextensions-apiserver v0.17.0/go.mod h1:XiIFUakZywkUl54fVXa7QTEHcqQz9HG55nHd1DCoHj8= -k8s.io/apiextensions-apiserver v0.31.2 h1:W8EwUb8+WXBLu56ser5IudT2cOho0gAKeTOnywBLxd0= -k8s.io/apiextensions-apiserver v0.31.2/go.mod h1:i+Geh+nGCJEGiCGR3MlBDkS7koHIIKWVfWeRFiOsUcM= +k8s.io/apiextensions-apiserver v0.31.3 h1:+GFGj2qFiU7rGCsA5o+p/rul1OQIq6oYpQw4+u+nciE= +k8s.io/apiextensions-apiserver v0.31.3/go.mod h1:2DSpFhUZZJmn/cr/RweH1cEVVbzFw9YBu4T+U3mf1e4= k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655/go.mod h1:nL6pwRT8NgfF8TT68DBI8uEePRt89cSvoXUVqbkWHq4= k8s.io/apimachinery v0.17.0/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= k8s.io/apimachinery v0.19.0/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlmA= -k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw= -k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/apimachinery v0.31.3 h1:6l0WhcYgasZ/wk9ktLq5vLaoXJJr5ts6lkaQzgeYPq4= +k8s.io/apimachinery v0.31.3/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= k8s.io/apiserver v0.0.0-20190918160949-bfa5e2e684ad/go.mod h1:XPCXEwhjaFN29a8NldXA901ElnKeKLrLtREO9ZhFyhg= k8s.io/apiserver v0.17.0/go.mod h1:ABM+9x/prjINN6iiffRVNCBR2Wk7uY4z+EtEGZD48cg= -k8s.io/apiserver v0.31.2 h1:VUzOEUGRCDi6kX1OyQ801m4A7AUPglpsmGvdsekmcI4= -k8s.io/apiserver v0.31.2/go.mod h1:o3nKZR7lPlJqkU5I3Ove+Zx3JuoFjQobGX1Gctw6XuE= +k8s.io/apiserver v0.31.3 h1:+1oHTtCB+OheqFEz375D0IlzHZ5VeQKX1KGXnx+TTuY= +k8s.io/apiserver v0.31.3/go.mod h1:PrxVbebxrxQPFhJk4powDISIROkNMKHibTg9lTRQ0Qg= k8s.io/cli-runtime v0.31.2 h1:7FQt4C4Xnqx8V1GJqymInK0FFsoC+fAZtbLqgXYVOLQ= k8s.io/cli-runtime v0.31.2/go.mod h1:XROyicf+G7rQ6FQJMbeDV9jqxzkWXTYD6Uxd15noe0Q= k8s.io/client-go v0.0.0-20190918160344-1fbdaa4c8d90/go.mod h1:J69/JveO6XESwVgG53q3Uz5OSfgsv4uxpScmmyYOOlk= k8s.io/client-go v0.17.0/go.mod h1:TYgR6EUHs6k45hb6KWjVD6jFZvJV4gHDikv/It0xz+k= k8s.io/client-go v0.19.0/go.mod h1:H9E/VT95blcFQnlyShFgnFT9ZnJOAceiUHM3MlRC+mU= -k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc= -k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs= +k8s.io/client-go v0.31.3 h1:CAlZuM+PH2cm+86LOBemaJI/lQ5linJ6UFxKX/SoG+4= +k8s.io/client-go v0.31.3/go.mod h1:2CgjPUTpv3fE5dNygAr2NcM8nhHzXvxB8KL5gYc3kJs= k8s.io/cluster-bootstrap v0.31.2 h1:tnycetMTbbCysYcx6AolV7DvPA/WXMnAYIl/vXIm7kM= k8s.io/cluster-bootstrap v0.31.2/go.mod h1:V4D+Zc7aJ5dcRYualA94kGN95ELRM61xegQpVN2ruY8= k8s.io/code-generator v0.0.0-20190912054826-cd179ad6a269/go.mod h1:V5BD6M4CyaN5m+VthcclXWsVcT1Hu+glwa1bi3MIsyE= @@ -2416,8 +2423,8 @@ k8s.io/code-generator v0.17.0/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+ k8s.io/code-generator v0.19.0/go.mod h1:moqLn7w0t9cMs4+5CQyxnfA/HV8MF6aAVENF+WZZhgk= k8s.io/component-base v0.0.0-20190918160511-547f6c5d7090/go.mod h1:933PBGtQFJky3TEwYx4aEPZ4IxqhWh3R6DCmzqIn1hA= k8s.io/component-base v0.17.0/go.mod h1:rKuRAokNMY2nn2A6LP/MiwpoaMRHpfRnrPaUJJj1Yoc= -k8s.io/component-base v0.31.2 h1:Z1J1LIaC0AV+nzcPRFqfK09af6bZ4D1nAOpWsy9owlA= -k8s.io/component-base v0.31.2/go.mod h1:9PeyyFN/drHjtJZMCTkSpQJS3U9OXORnHQqMLDz0sUQ= +k8s.io/component-base v0.31.3 h1:DMCXXVx546Rfvhj+3cOm2EUxhS+EyztH423j+8sOwhQ= +k8s.io/component-base v0.31.3/go.mod h1:xME6BHfUOafRgT0rGVBGl7TuSg8Z9/deT7qq6w7qjIU= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= @@ -2446,8 +2453,8 @@ k8s.io/metrics v0.31.2/go.mod h1:QqqyReApEWO1UEgXOSXiHCQod6yTxYctbAAQBWZkboU= k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= k8s.io/utils v0.0.0-20200729134348-d5654de09c73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= -k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 h1:MDF6h2H/h4tbzmtIKTuctcwZmY0tY9mD9fNT47QO6HI= +k8s.io/utils v0.0.0-20240921022957-49e7df575cb6/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= @@ -2469,8 +2476,8 @@ rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/application v0.8.3 h1:5UETobiVhxTkKn3pIESImXiMNmSg3VkM5+JvmYGDPko= sigs.k8s.io/application v0.8.3/go.mod h1:Mv+ht9RE/QNtITYCzRbt3XTIN6t6so6cInmiyg6wOIg= sigs.k8s.io/controller-runtime v0.4.0/go.mod h1:ApC79lpY3PHW9xj/w9pj+lYkLgwAAUZwfXkME1Lajns= -sigs.k8s.io/controller-runtime v0.19.1 h1:Son+Q40+Be3QWb+niBXAg2vFiYWolDjjRfO8hn/cxOk= -sigs.k8s.io/controller-runtime v0.19.1/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= +sigs.k8s.io/controller-runtime v0.19.3 h1:XO2GvC9OPftRst6xWCpTgBZO04S2cbp0Qqkj8bX1sPw= +sigs.k8s.io/controller-runtime v0.19.3/go.mod h1:j4j87DqtsThvwTv5/Tc5NFRyyF/RF0ip4+62tbTSIUM= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo= diff --git a/migrations/tables/plan.yaml b/migrations/tables/plan.yaml new file mode 100644 index 0000000000..898c95136a --- /dev/null +++ b/migrations/tables/plan.yaml @@ -0,0 +1,37 @@ +apiVersion: schemas.schemahero.io/v1alpha4 +kind: Table +metadata: + name: plan +spec: + name: plan + schema: + rqlite: + strict: true + primaryKey: + - app_id + - version_label + columns: + - name: app_id + type: text + constraints: + notNull: true + - name: version_label + type: text + constraints: + notNull: true + - name: created_at + type: integer + constraints: + notNull: true + - name: updated_at + type: integer + constraints: + notNull: true + - name: status + type: text + constraints: + notNull: true + - name: plan + type: text + constraints: + notNull: true diff --git a/pkg/apiserver/server.go b/pkg/apiserver/server.go index 93f507612b..b9320e6b0e 100644 --- a/pkg/apiserver/server.go +++ b/pkg/apiserver/server.go @@ -20,6 +20,7 @@ import ( "github.com/replicatedhq/kots/pkg/operator" operatorclient "github.com/replicatedhq/kots/pkg/operator/client" "github.com/replicatedhq/kots/pkg/persistence" + "github.com/replicatedhq/kots/pkg/plan" "github.com/replicatedhq/kots/pkg/policy" "github.com/replicatedhq/kots/pkg/rbac" "github.com/replicatedhq/kots/pkg/reporting" @@ -136,6 +137,10 @@ func Start(params *APIServerParams) { log.Println("Failed to start session purge cron job:", err) } + if err := plan.Resume(store.GetStore()); err != nil { + log.Println("Failed to resume plan:", err) + } + waitForAirgap, err := automation.NeedToWaitForAirgapApp() if err != nil { log.Println("Failed to check if airgap install is in progress:", err) diff --git a/pkg/embeddedcluster/monitor.go b/pkg/embeddedcluster/monitor.go index add938581d..5264b744af 100644 --- a/pkg/embeddedcluster/monitor.go +++ b/pkg/embeddedcluster/monitor.go @@ -50,7 +50,7 @@ func RequiresClusterUpgrade(ctx context.Context, kbClient kbclient.Client, kotsK func StartClusterUpgrade(ctx context.Context, kotsKinds *kotsutil.KotsKinds, registrySettings registrytypes.RegistrySettings) error { spec := kotsKinds.EmbeddedClusterConfig.Spec - artifacts := getArtifactsFromInstallation(kotsKinds.Installation) + artifacts := GetArtifactsFromInstallation(kotsKinds.Installation) if err := startClusterUpgrade(ctx, spec, artifacts, registrySettings, *kotsKinds.License, kotsKinds.Installation.Spec.VersionLabel); err != nil { return fmt.Errorf("failed to start cluster upgrade: %w", err) diff --git a/pkg/embeddedcluster/util.go b/pkg/embeddedcluster/util.go index 94d0a0eb1d..37203e94a5 100644 --- a/pkg/embeddedcluster/util.go +++ b/pkg/embeddedcluster/util.go @@ -171,7 +171,7 @@ func GetSeaweedFSS3ServiceIP(ctx context.Context, kbClient kbclient.Client) (str return svc.Spec.ClusterIP, nil } -func getArtifactsFromInstallation(installation kotsv1beta1.Installation) *embeddedclusterv1beta1.ArtifactsLocation { +func GetArtifactsFromInstallation(installation kotsv1beta1.Installation) *embeddedclusterv1beta1.ArtifactsLocation { if installation.Spec.EmbeddedClusterArtifacts == nil { return nil } diff --git a/pkg/embeddedcluster/util_test.go b/pkg/embeddedcluster/util_test.go index beddb6885b..356f356c25 100644 --- a/pkg/embeddedcluster/util_test.go +++ b/pkg/embeddedcluster/util_test.go @@ -8,7 +8,7 @@ import ( kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" ) -func Test_getArtifactsFromInstallation(t *testing.T) { +func Test_GetArtifactsFromInstallation(t *testing.T) { type args struct { installation kotsv1beta1.Installation } @@ -56,9 +56,9 @@ func Test_getArtifactsFromInstallation(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := getArtifactsFromInstallation(tt.args.installation) + got := GetArtifactsFromInstallation(tt.args.installation) if !reflect.DeepEqual(got, tt.want) { - t.Errorf("getArtifactsFromInstallation() = %v, want %v", got, tt.want) + t.Errorf("GetArtifactsFromInstallation() = %v, want %v", got, tt.want) } }) } diff --git a/pkg/handlers/deploy_ec2.go b/pkg/handlers/deploy_ec2.go new file mode 100644 index 0000000000..fe7788a62f --- /dev/null +++ b/pkg/handlers/deploy_ec2.go @@ -0,0 +1,244 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/gorilla/mux" + "github.com/pkg/errors" + apptypes "github.com/replicatedhq/kots/pkg/app/types" + "github.com/replicatedhq/kots/pkg/k8sutil" + "github.com/replicatedhq/kots/pkg/kotsutil" + "github.com/replicatedhq/kots/pkg/logger" + "github.com/replicatedhq/kots/pkg/plan" + plantypes "github.com/replicatedhq/kots/pkg/plan/types" + "github.com/replicatedhq/kots/pkg/replicatedapp" + "github.com/replicatedhq/kots/pkg/store" + "github.com/replicatedhq/kots/pkg/update" +) + +type DeployEC2AppVersionRequest struct { + VersionLabel string `json:"versionLabel"` + UpdateCursor string `json:"updateCursor"` + ChannelID string `json:"channelId"` +} + +type DeployEC2AppVersionResponse struct { + Success bool `json:"success"` + Error string `json:"error,omitempty"` +} + +type UpdatePlanStepRequest struct { + VersionLabel string `json:"versionLabel"` + Status plantypes.PlanStepStatus `json:"status"` + StatusDescription string `json:"statusDescription"` + Output string `json:"output"` +} + +type UpdatePlanStepResponse struct { + Success bool `json:"success"` + Error string `json:"error,omitempty"` +} + +type GetEC2DeployStatusResponse struct { + Step string `json:"step"` + Status string `json:"status"` + CurrentMessage string `json:"currentMessage"` +} + +func (h *Handler) DeployEC2AppVersion(w http.ResponseWriter, r *http.Request) { + response := DeployEC2AppVersionResponse{ + Success: false, + } + + request := DeployEC2AppVersionRequest{} + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + response.Error = "failed to decode request body" + logger.Error(errors.Wrap(err, response.Error)) + JSON(w, http.StatusBadRequest, response) + return + } + + appSlug := mux.Vars(r)["appSlug"] + + foundApp, err := store.GetStore().GetAppFromSlug(appSlug) + if err != nil { + response.Error = "failed to get app from slug" + logger.Error(errors.Wrap(err, response.Error)) + JSON(w, http.StatusInternalServerError, response) + return + } + + canDeploy, reason, err := canDeployEC2AppVersion(foundApp, request) + if err != nil { + response.Error = "failed to check if upgrade service can start" + logger.Error(errors.Wrap(err, response.Error)) + JSON(w, http.StatusInternalServerError, response) + return + } + if !canDeploy { + response.Error = reason + logger.Error(errors.New(response.Error)) + JSON(w, http.StatusBadRequest, response) + return + } + + kbClient, err := k8sutil.GetKubeClient(r.Context()) + if err != nil { + response.Error = "failed to get kube client" + logger.Error(errors.Wrap(err, response.Error)) + JSON(w, http.StatusInternalServerError, response) + return + } + + p, err := plan.PlanUpgrade(store.GetStore(), kbClient, plan.PlanUpgradeOptions{ + AppSlug: appSlug, + VersionLabel: request.VersionLabel, + UpdateCursor: request.UpdateCursor, + ChannelID: request.ChannelID, + }) + if err != nil { + response.Error = "failed to plan upgrade" + logger.Error(errors.Wrap(err, response.Error)) + JSON(w, http.StatusInternalServerError, response) + return + } + + if err := store.GetStore().UpsertPlan(p); err != nil { + response.Error = "failed to upsert plan" + logger.Error(errors.Wrap(err, response.Error)) + JSON(w, http.StatusInternalServerError, response) + return + } + + go func() { + if err := plan.Execute(store.GetStore(), p); err != nil { + logger.Error(errors.Wrapf(err, "failed to execute plan %s", p.ID)) + } + }() + + response.Success = true + + JSON(w, http.StatusOK, response) +} + +func canDeployEC2AppVersion(a *apptypes.App, r DeployEC2AppVersionRequest) (bool, string, error) { + currLicense, err := kotsutil.LoadLicenseFromBytes([]byte(a.License)) + if err != nil { + return false, "", errors.Wrap(err, "failed to parse app license") + } + + if a.IsAirgap { + updateBundle, err := update.GetAirgapUpdate(a.Slug, r.ChannelID, r.UpdateCursor) + if err != nil { + return false, "", errors.Wrap(err, "failed to get airgap update") + } + airgap, err := kotsutil.FindAirgapMetaInBundle(updateBundle) + if err != nil { + return false, "", errors.Wrap(err, "failed to find airgap metadata") + } + if _, err := kotsutil.FindChannelInLicense(airgap.Spec.ChannelID, currLicense); err != nil { + return false, "channel mismatch, channel not in license", nil + } + if r.ChannelID != airgap.Spec.ChannelID { + return false, "channel mismatch", nil + } + isDeployable, nonDeployableCause, err := update.IsAirgapUpdateDeployable(a, airgap) + if err != nil { + return false, "", errors.Wrap(err, "failed to check if airgap update is deployable") + } + if !isDeployable { + return false, nonDeployableCause, nil + } + return true, "", nil + } + + ll, err := replicatedapp.GetLatestLicense(currLicense, a.SelectedChannelID) + if err != nil { + return false, "", errors.Wrap(err, "failed to get latest license") + } + if currLicense.Spec.ChannelID != ll.License.Spec.ChannelID || r.ChannelID != ll.License.Spec.ChannelID { + return false, "license channel has changed, please sync the license", nil + } + updates, err := update.GetAvailableUpdates(store.GetStore(), a, currLicense) + if err != nil { + return false, "", errors.Wrap(err, "failed to get available updates") + } + isDeployable, nonDeployableCause := false, "update not found" + for _, u := range updates { + if u.UpdateCursor == r.UpdateCursor { + isDeployable, nonDeployableCause = u.IsDeployable, u.NonDeployableCause + break + } + } + if !isDeployable { + return false, nonDeployableCause, nil + } + return true, "", nil +} + +func (h *Handler) UpdatePlanStep(w http.ResponseWriter, r *http.Request) { + response := UpdatePlanStepResponse{ + Success: false, + } + + request := UpdatePlanStepRequest{} + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + response.Error = "failed to decode request body" + logger.Error(errors.Wrap(err, response.Error)) + JSON(w, http.StatusBadRequest, response) + return + } + + appSlug := mux.Vars(r)["appSlug"] + stepID := mux.Vars(r)["stepID"] + + opts := plan.UpdateStepOptions{ + AppSlug: appSlug, + VersionLabel: request.VersionLabel, + StepID: stepID, + Status: request.Status, + StatusDescription: request.StatusDescription, + Output: request.Output, + } + if err := plan.UpdateStep(store.GetStore(), opts); err != nil { + response.Error = "failed to update plan step" + logger.Error(errors.Wrap(err, response.Error)) + JSON(w, http.StatusInternalServerError, response) + return + } + + response.Success = true + JSON(w, http.StatusOK, response) +} + +func (h *Handler) GetEC2DeployStatus(w http.ResponseWriter, r *http.Request) { + appSlug := mux.Vars(r)["appSlug"] + + a, err := store.GetStore().GetAppFromSlug(appSlug) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + logger.Error(errors.Wrap(err, "failed to get app")) + return + } + + p, updatedAt, err := store.GetStore().GetCurrentPlan(a.ID) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + logger.Error(errors.Wrap(err, "failed to get active plan")) + return + } + if p == nil || time.Since(*updatedAt) > time.Minute { + JSON(w, http.StatusOK, GetEC2DeployStatusResponse{}) + return + } + + currStep := p.CurrentStep() + + JSON(w, http.StatusOK, GetEC2DeployStatusResponse{ + Step: string(currStep.Type), + Status: string(currStep.Status), + CurrentMessage: currStep.StatusDescription, + }) +} diff --git a/pkg/handlers/handlers.go b/pkg/handlers/handlers.go index 6d1e190c59..fe8c9a9bef 100644 --- a/pkg/handlers/handlers.go +++ b/pkg/handlers/handlers.go @@ -335,6 +335,12 @@ func RegisterSessionAuthRoutes(r *mux.Router, kotsStore store.Store, handler KOT r.Name("GetDebugInfo").Path("/api/v1/debug").Methods("GET"). HandlerFunc(middleware.EnforceAccess(policy.ClusterRead, handler.GetDebugInfo)) + // endpoints for EC install2 workflow + r.Name("DeployEC2AppVersion").Path("/api/v1/app/{appSlug}/ec2-deploy").Methods("POST"). + HandlerFunc(middleware.EnforceAccess(policy.AppUpdate, handler.DeployEC2AppVersion)) + r.Name("GetEC2DeployStatus").Path("/api/v1/app/{appSlug}/ec2-deploy/status").Methods("GET"). + HandlerFunc(middleware.EnforceAccess(policy.AppUpdate, handler.GetEC2DeployStatus)) + // Upgrade service r.Name("StartUpgradeService").Path("/api/v1/app/{appSlug}/start-upgrade-service").Methods("POST"). HandlerFunc(middleware.EnforceAccess(policy.AppUpdate, handler.StartUpgradeService)) @@ -391,6 +397,10 @@ func RegisterUnauthenticatedRoutes(handler *Handler, kotsStore store.Store, debu // This handler requires a valid token in the query loggingRouter.Path("/api/v1/embedded-cluster/join").Methods("GET").HandlerFunc(handler.GetEmbeddedClusterNodeJoinCommand) + + // TODO (@salah): make this authed + // endpoints for EC install2 workflow + loggingRouter.Path("/api/v1/app/{appSlug}/plan/{stepID}").Methods("PUT").HandlerFunc(handler.UpdatePlanStep) } func StreamJSON(c *websocket.Conn, payload interface{}) { diff --git a/pkg/handlers/handlers_test.go b/pkg/handlers/handlers_test.go index c569dceebc..7bf0bc9a1c 100644 --- a/pkg/handlers/handlers_test.go +++ b/pkg/handlers/handlers_test.go @@ -1440,6 +1440,19 @@ var HandlerPolicyTests = map[string][]HandlerPolicyTest{ }, }, + // endpoint for EC install2 workflow + "DeployEC2AppVersion": { + { + Vars: map[string]string{"appSlug": "my-app"}, + Roles: []rbactypes.Role{rbac.ClusterAdminRole}, + SessionRoles: []string{rbac.ClusterAdminRoleID}, + Calls: func(storeRecorder *mock_store.MockStoreMockRecorder, handlerRecorder *mock_handlers.MockKOTSHandlerMockRecorder) { + handlerRecorder.DeployEC2AppVersion(gomock.Any(), gomock.Any()) + }, + ExpectStatus: http.StatusOK, + }, + }, + // Upgrade Service "StartUpgradeService": { { @@ -1463,6 +1476,17 @@ var HandlerPolicyTests = map[string][]HandlerPolicyTest{ ExpectStatus: http.StatusOK, }, }, + "GetEC2DeployStatus": { + { + Vars: map[string]string{"appSlug": "my-app"}, + Roles: []rbactypes.Role{rbac.ClusterAdminRole}, + SessionRoles: []string{rbac.ClusterAdminRoleID}, + Calls: func(storeRecorder *mock_store.MockStoreMockRecorder, handlerRecorder *mock_handlers.MockKOTSHandlerMockRecorder) { + handlerRecorder.GetEC2DeployStatus(gomock.Any(), gomock.Any()) + }, + ExpectStatus: http.StatusOK, + }, + }, "UpgradeServiceProxy": {}, // Not implemented } diff --git a/pkg/handlers/interface.go b/pkg/handlers/interface.go index e4e8085f88..ee7aa3690c 100644 --- a/pkg/handlers/interface.go +++ b/pkg/handlers/interface.go @@ -167,6 +167,11 @@ type KOTSHandler interface { // Debug info GetDebugInfo(w http.ResponseWriter, r *http.Request) + // endpoints for EC install2 workflow + DeployEC2AppVersion(w http.ResponseWriter, r *http.Request) + GetEC2DeployStatus(w http.ResponseWriter, r *http.Request) + UpdatePlanStep(w http.ResponseWriter, r *http.Request) + // Upgrade service StartUpgradeService(w http.ResponseWriter, r *http.Request) GetUpgradeServiceStatus(w http.ResponseWriter, r *http.Request) diff --git a/pkg/handlers/metadata.go b/pkg/handlers/metadata.go index b6e44d1b96..002adcc484 100644 --- a/pkg/handlers/metadata.go +++ b/pkg/handlers/metadata.go @@ -56,6 +56,7 @@ type AdminConsoleMetadata struct { IsAirgap bool `json:"isAirgap"` IsKurl bool `json:"isKurl"` IsEmbeddedCluster bool `json:"isEmbeddedCluster"` + IsEC2Install bool `json:"isEC2Install"` } // GetMetadataHandler helper function that returns a http handler func that returns metadata. It takes a function that @@ -77,6 +78,7 @@ func GetMetadataHandler(getK8sInfoFn MetadataK8sFn, kotsStore store.Store) http. metadataResponse.AdminConsoleMetadata.IsAirgap = kotsadmMetadata.IsAirgap metadataResponse.AdminConsoleMetadata.IsKurl = kotsadmMetadata.IsKurl metadataResponse.AdminConsoleMetadata.IsEmbeddedCluster = kotsadmMetadata.IsEmbeddedCluster + metadataResponse.AdminConsoleMetadata.IsEC2Install = util.IsEC2Install() logger.Info(fmt.Sprintf("config map %q not found", metadataConfigMapName)) JSON(w, http.StatusOK, &metadataResponse) @@ -118,6 +120,7 @@ func GetMetadataHandler(getK8sInfoFn MetadataK8sFn, kotsStore store.Store) http. IsAirgap: kotsadmMetadata.IsAirgap, IsKurl: kotsadmMetadata.IsKurl, IsEmbeddedCluster: kotsadmMetadata.IsEmbeddedCluster, + IsEC2Install: util.IsEC2Install(), } if kotsadmMetadata.IsEmbeddedCluster { diff --git a/pkg/handlers/mock/mock.go b/pkg/handlers/mock/mock.go index e654f6696a..305aeca5e6 100644 --- a/pkg/handlers/mock/mock.go +++ b/pkg/handlers/mock/mock.go @@ -334,6 +334,18 @@ func (mr *MockKOTSHandlerMockRecorder) DeployAppVersion(w, r interface{}) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeployAppVersion", reflect.TypeOf((*MockKOTSHandler)(nil).DeployAppVersion), w, r) } +// DeployEC2AppVersion mocks base method. +func (m *MockKOTSHandler) DeployEC2AppVersion(w http.ResponseWriter, r *http.Request) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "DeployEC2AppVersion", w, r) +} + +// DeployEC2AppVersion indicates an expected call of DeployEC2AppVersion. +func (mr *MockKOTSHandlerMockRecorder) DeployEC2AppVersion(w, r interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeployEC2AppVersion", reflect.TypeOf((*MockKOTSHandler)(nil).DeployEC2AppVersion), w, r) +} + // DisableAppGitOps mocks base method. func (m *MockKOTSHandler) DisableAppGitOps(w http.ResponseWriter, r *http.Request) { m.ctrl.T.Helper() @@ -730,6 +742,18 @@ func (mr *MockKOTSHandlerMockRecorder) GetDownstreamOutput(w, r interface{}) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDownstreamOutput", reflect.TypeOf((*MockKOTSHandler)(nil).GetDownstreamOutput), w, r) } +// GetEC2DeployStatus mocks base method. +func (m *MockKOTSHandler) GetEC2DeployStatus(w http.ResponseWriter, r *http.Request) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "GetEC2DeployStatus", w, r) +} + +// GetEC2DeployStatus indicates an expected call of GetEC2DeployStatus. +func (mr *MockKOTSHandlerMockRecorder) GetEC2DeployStatus(w, r interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEC2DeployStatus", reflect.TypeOf((*MockKOTSHandler)(nil).GetEC2DeployStatus), w, r) +} + // GetEmbeddedClusterNode mocks base method. func (m *MockKOTSHandler) GetEmbeddedClusterNode(w http.ResponseWriter, r *http.Request) { m.ctrl.T.Helper() @@ -1534,6 +1558,18 @@ func (mr *MockKOTSHandlerMockRecorder) UpdateGlobalSnapshotSettings(w, r interfa return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateGlobalSnapshotSettings", reflect.TypeOf((*MockKOTSHandler)(nil).UpdateGlobalSnapshotSettings), w, r) } +// UpdatePlanStep mocks base method. +func (m *MockKOTSHandler) UpdatePlanStep(w http.ResponseWriter, r *http.Request) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UpdatePlanStep", w, r) +} + +// UpdatePlanStep indicates an expected call of UpdatePlanStep. +func (mr *MockKOTSHandlerMockRecorder) UpdatePlanStep(w, r interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePlanStep", reflect.TypeOf((*MockKOTSHandler)(nil).UpdatePlanStep), w, r) +} + // UpdateRedact mocks base method. func (m *MockKOTSHandler) UpdateRedact(w http.ResponseWriter, r *http.Request) { m.ctrl.T.Helper() diff --git a/pkg/kotsadmsnapshot/backup_test.go b/pkg/kotsadmsnapshot/backup_test.go index 8c4889f9e5..7e86113bac 100644 --- a/pkg/kotsadmsnapshot/backup_test.go +++ b/pkg/kotsadmsnapshot/backup_test.go @@ -2669,6 +2669,7 @@ func Test_getInstanceBackupMetadata(t *testing.T) { }, Spec: embeddedclusterv1beta1.InstallationSpec{ BinaryName: "my-app", + SourceType: embeddedclusterv1beta1.InstallationSourceTypeCRD, }, } seaweedFSS3Service := &corev1.Service{ diff --git a/pkg/kotsutil/kots.go b/pkg/kotsutil/kots.go index 4b69b7f5a8..08357cefde 100644 --- a/pkg/kotsutil/kots.go +++ b/pkg/kotsutil/kots.go @@ -933,6 +933,20 @@ func LoadV1Beta2HelmChartFromContents(content []byte) (*kotsv1beta2.HelmChart, e return obj.(*kotsv1beta2.HelmChart), nil } +func FindInstallationInPath(fromDir string) (*kotsv1beta1.Installation, error) { + objects, err := loadRuntimeObjectsFromPath("kots.io/v1beta1", "Installation", fromDir) + if err != nil { + return nil, errors.Wrapf(err, "failed to load Installation from path %s", fromDir) + } + + if len(objects) == 0 { + return nil, errors.New("not found") + } + + // we only support having one installation spec + return objects[0].(*kotsv1beta1.Installation), nil +} + func LoadInstallationFromContents(installationData []byte) (*kotsv1beta1.Installation, error) { decode := scheme.Codecs.UniversalDeserializer().Decode obj, gvk, err := decode([]byte(installationData), nil, nil) diff --git a/pkg/plan/app.go b/pkg/plan/app.go new file mode 100644 index 0000000000..61cf2a8f50 --- /dev/null +++ b/pkg/plan/app.go @@ -0,0 +1,217 @@ +package plan + +import ( + "fmt" + "os" + "strconv" + + "github.com/phayes/freeport" + "github.com/pkg/errors" + "github.com/replicatedhq/kots/pkg/filestore" + "github.com/replicatedhq/kots/pkg/kotsutil" + "github.com/replicatedhq/kots/pkg/operator" + "github.com/replicatedhq/kots/pkg/plan/types" + "github.com/replicatedhq/kots/pkg/render" + "github.com/replicatedhq/kots/pkg/reporting" + "github.com/replicatedhq/kots/pkg/store" + "github.com/replicatedhq/kots/pkg/update" + "github.com/replicatedhq/kots/pkg/upgradeservice" + upgradeservicetypes "github.com/replicatedhq/kots/pkg/upgradeservice/types" + "github.com/replicatedhq/kots/pkg/util" +) + +func executeAppUpgradeService(s store.Store, p *types.Plan, step *types.PlanStep) (finalError error) { + in, ok := step.Input.(types.PlanStepInputAppUpgradeService) + if !ok { + return errors.New("invalid input for app upgrade service step") + } + + if err := UpdateStep(s, UpdateStepOptions{ + AppSlug: p.AppSlug, + VersionLabel: p.VersionLabel, + StepID: step.ID, + Status: types.StepStatusStarting, + StatusDescription: "Preparing...", + }); err != nil { + return errors.Wrap(err, "update step status") + } + + // TODO (@salah): don't run as separate process if kots version did not change? + if err := upgradeservice.Start(in.Params); err != nil { + return errors.Wrap(err, "start app upgrade service") + } + + if err := UpdateStep(s, UpdateStepOptions{ + AppSlug: p.AppSlug, + VersionLabel: p.VersionLabel, + StepID: step.ID, + Status: types.StepStatusRunning, + }); err != nil { + return errors.Wrap(err, "update step status") + } + + return nil +} + +func executeAppUpgrade(s store.Store, p *types.Plan, step *types.PlanStep) error { + if err := UpdateStep(s, UpdateStepOptions{ + AppSlug: p.AppSlug, + VersionLabel: p.VersionLabel, + StepID: step.ID, + Status: types.StepStatusRunning, + }); err != nil { + return errors.Wrap(err, "update step status") + } + + ausOutput, err := getAppUpgradeServiceOutput(p) + if err != nil { + return errors.Wrap(err, "get app upgrade service output") + } + appArchive, err := getAppArchive(ausOutput["app-version-archive"]) + if err != nil { + return errors.Wrap(err, "get app archive") + } + defer os.RemoveAll(appArchive) + + skipPreflights, err := strconv.ParseBool(ausOutput["skip-preflights"]) + if err != nil { + return errors.Wrap(err, "failed to parse is skip preflights") + } + + sequence, err := s.CreateAppVersion(p.AppID, &p.BaseSequence, appArchive, ausOutput["source"], false, false, "", skipPreflights, render.Renderer{}) + if err != nil { + return errors.Wrap(err, "create app version") + } + + if p.IsAirgap { + if err := update.RemoveAirgapUpdate(p.AppSlug, p.ChannelID, p.UpdateCursor); err != nil { + return errors.Wrap(err, "remove airgap update") + } + } + + if err := filestore.GetStore().DeleteArchive(ausOutput["app-version-archive"]); err != nil { + return errors.Wrap(err, "delete archive") + } + + if ausOutput["preflight-result"] != "" { + if err := s.SetPreflightResults(p.AppID, sequence, []byte(ausOutput["preflight-result"])); err != nil { + return errors.Wrap(err, "set preflight results") + } + } + + if err := s.SetAppChannelChanged(p.AppID, false); err != nil { + return errors.Wrap(err, "reset channel changed flag") + } + + if err := s.MarkAsCurrentDownstreamVersion(p.AppID, sequence); err != nil { + return errors.Wrap(err, "mark as current downstream version") + } + + go operator.MustGetOperator().DeployApp(p.AppID, sequence) + + if err := UpdateStep(s, UpdateStepOptions{ + AppSlug: p.AppSlug, + VersionLabel: p.VersionLabel, + StepID: step.ID, + Status: types.StepStatusComplete, + }); err != nil { + return errors.Wrap(err, "update step status") + } + + return nil +} + +func getAppUpgradeServiceInput(s store.Store, p *types.Plan, stepID string) (*types.PlanStepInputAppUpgradeService, error) { + a, err := s.GetAppFromSlug(p.AppSlug) + if err != nil { + return nil, errors.Wrap(err, "get app from slug") + } + + registrySettings, err := s.GetRegistryDetailsForApp(a.ID) + if err != nil { + return nil, errors.Wrap(err, "get registry details for app") + } + + baseArchive, baseSequence, err := s.GetAppVersionBaseArchive(a.ID, p.VersionLabel) + if err != nil { + return nil, errors.Wrap(err, "get app version base archive") + } + + nextSequence, err := s.GetNextAppSequence(a.ID) + if err != nil { + return nil, errors.Wrap(err, "get next app sequence") + } + + source := "Upstream Update" + if a.IsAirgap { + source = "Airgap Update" + } + + var updateKOTSBin string + var updateAirgapBundle string + + if a.IsAirgap { + au, err := update.GetAirgapUpdate(a.Slug, p.ChannelID, p.UpdateCursor) + if err != nil { + return nil, errors.Wrap(err, "get airgap update") + } + updateAirgapBundle = au + kb, err := kotsutil.GetKOTSBinFromAirgapBundle(au) + if err != nil { + return nil, errors.Wrap(err, "get kots binary from airgap bundle") + } + updateKOTSBin = kb + } else { + // TODO (@salah): revert this + // TODO (@salah): no need to download if the kots version did not change? + // TODO (@salah): how to know if the kots version did not change? (i think there's a replicated.app endpoint for this) + // kb, err := replicatedapp.DownloadKOTSBinary(license, p.VersionLabel) + // if err != nil { + // return nil, errors.Wrap(err, "download kots binary") + // } + updateKOTSBin = kotsutil.GetKOTSBinPath() + } + + port, err := freeport.GetFreePort() + if err != nil { + return nil, errors.Wrap(err, "get free port") + } + + ausParams := upgradeservicetypes.UpgradeServiceParams{ + Port: fmt.Sprintf("%d", port), + PlanStepID: stepID, + + AppID: a.ID, + AppSlug: a.Slug, + AppName: a.Name, + AppIsAirgap: a.IsAirgap, + AppIsGitOps: a.IsGitOps, + AppLicense: a.License, + AppArchive: baseArchive, + + Source: source, + BaseSequence: baseSequence, + NextSequence: nextSequence, + + UpdateVersionLabel: p.VersionLabel, + UpdateCursor: p.UpdateCursor, + UpdateChannelID: p.ChannelID, + UpdateECVersion: p.NewECVersion, + UpdateKOTSBin: updateKOTSBin, + UpdateAirgapBundle: updateAirgapBundle, + + CurrentECVersion: util.EmbeddedClusterVersion(), + + RegistryEndpoint: registrySettings.Hostname, + RegistryUsername: registrySettings.Username, + RegistryPassword: registrySettings.Password, + RegistryNamespace: registrySettings.Namespace, + RegistryIsReadOnly: registrySettings.IsReadOnly, + + ReportingInfo: reporting.GetReportingInfo(a.ID), + } + + return &types.PlanStepInputAppUpgradeService{ + Params: ausParams, + }, nil +} diff --git a/pkg/plan/cluster.go b/pkg/plan/cluster.go new file mode 100644 index 0000000000..2f00a89a76 --- /dev/null +++ b/pkg/plan/cluster.go @@ -0,0 +1,116 @@ +package plan + +import ( + "context" + "reflect" + "time" + + "github.com/pkg/errors" + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + apptypes "github.com/replicatedhq/kots/pkg/app/types" + "github.com/replicatedhq/kots/pkg/embeddedcluster" + "github.com/replicatedhq/kots/pkg/kotsutil" + "github.com/replicatedhq/kots/pkg/plan/types" + "github.com/replicatedhq/kots/pkg/store" + "github.com/replicatedhq/kots/pkg/websocket" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kbclient "sigs.k8s.io/controller-runtime/pkg/client" + k8syaml "sigs.k8s.io/yaml" +) + +func executeECUpgrade(s store.Store, p *types.Plan, step *types.PlanStep) error { + in, ok := step.Input.(types.PlanStepInputECUpgrade) + if !ok { + return errors.New("invalid input for embedded cluster upgrade step") + } + + newInstall := &ecv1beta1.Installation{ + TypeMeta: metav1.TypeMeta{ + APIVersion: ecv1beta1.GroupVersion.String(), + Kind: "Installation", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: time.Now().Format("20060102150405"), + Labels: map[string]string{ + "replicated.com/disaster-recovery": "ec-install", + }, + }, + Spec: in.CurrentECInstallation.Spec, + } + newInstall.Spec.Artifacts = embeddedcluster.GetArtifactsFromInstallation(in.CurrentKOTSInstallation) + newInstall.Spec.Config = &in.NewECConfigSpec + newInstall.Spec.LicenseInfo = &ecv1beta1.LicenseInfo{IsDisasterRecoverySupported: in.IsDisasterRecoverySupported} + + if err := websocket.UpgradeCluster(newInstall, p.AppSlug, p.VersionLabel, step.ID); err != nil { + return errors.Wrap(err, "upgrade cluster") + } + + return nil +} + +func requiresECUpgrade(kcli kbclient.Client, newSpec *ecv1beta1.ConfigSpec) (bool, error) { + currInstall, err := embeddedcluster.GetCurrentInstallation(context.Background(), kcli) + if err != nil { + return false, errors.Wrap(err, "get current embedded cluster installation") + } + currSpec := currInstall.Spec.Config + + if currSpec.Version != newSpec.Version { + return true, nil + } + if currSpec.BinaryOverrideURL != newSpec.BinaryOverrideURL { + return true, nil + } + if currSpec.MetadataOverrideURL != newSpec.MetadataOverrideURL { + return true, nil + } + if !reflect.DeepEqual(currSpec.UnsupportedOverrides, newSpec.UnsupportedOverrides) { + return true, nil + } + return false, nil +} + +func getECUpgradeInput(s store.Store, kcli kbclient.Client, a *apptypes.App, versionLabel string, newSpec *ecv1beta1.ConfigSpec) (*types.PlanStepInputECUpgrade, error) { + license, err := kotsutil.LoadLicenseFromBytes([]byte(a.License)) + if err != nil { + return nil, errors.Wrap(err, "parse app license") + } + + baseArchive, _, err := s.GetAppVersionBaseArchive(a.ID, versionLabel) + if err != nil { + return nil, errors.Wrap(err, "get app version base archive") + } + + currKOTSInstall, err := kotsutil.FindInstallationInPath(baseArchive) + if err != nil { + return nil, errors.Wrap(err, "find kots installation in base archive") + } + + currECInstall, err := embeddedcluster.GetCurrentInstallation(context.Background(), kcli) + if err != nil { + return nil, errors.Wrap(err, "get current embedded cluster installation") + } + + return &types.PlanStepInputECUpgrade{ + CurrentECInstallation: *currECInstall, + CurrentKOTSInstallation: *currKOTSInstall, + NewECConfigSpec: *newSpec, + IsDisasterRecoverySupported: license.Spec.IsDisasterRecoverySupported, + }, nil +} + +func findECConfigSpecInRelease(manifests map[string][]byte) (*ecv1beta1.ConfigSpec, error) { + for _, contents := range manifests { + if !kotsutil.IsApiVersionKind(contents, "embeddedcluster.replicated.com/v1beta1", "Config") { + continue + } + + var cfg ecv1beta1.Config + if err := k8syaml.Unmarshal(contents, &cfg); err != nil { + return nil, errors.Wrap(err, "unmarshal") + } + return &cfg.Spec, nil + } + + return nil, errors.New("not found") +} diff --git a/pkg/plan/extensions.go b/pkg/plan/extensions.go new file mode 100644 index 0000000000..7b7c52fa16 --- /dev/null +++ b/pkg/plan/extensions.go @@ -0,0 +1,104 @@ +package plan + +import ( + "context" + "reflect" + + "github.com/pkg/errors" + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/kots/pkg/embeddedcluster" + "github.com/replicatedhq/kots/pkg/plan/types" + "github.com/replicatedhq/kots/pkg/websocket" + kbclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +type ExtensionsDiffResult struct { + Added []ecv1beta1.Chart + Removed []ecv1beta1.Chart + Modified []ecv1beta1.Chart +} + +func executeECExtensionAdd(p *types.Plan, step *types.PlanStep) error { + in, ok := step.Input.(types.PlanStepInputECExtension) + if !ok { + return errors.New("invalid input for embedded cluster extension add step") + } + if err := websocket.AddExtension(in.Repos, in.Chart, p.AppSlug, p.VersionLabel, step.ID); err != nil { + return errors.Wrap(err, "add extension") + } + return nil +} + +func executeECExtensionUpgrade(p *types.Plan, step *types.PlanStep) error { + in, ok := step.Input.(types.PlanStepInputECExtension) + if !ok { + return errors.New("invalid input for embedded cluster extension upgrade step") + } + if err := websocket.UpgradeExtension(in.Repos, in.Chart, p.AppSlug, p.VersionLabel, step.ID); err != nil { + return errors.Wrap(err, "upgrade extension") + } + return nil +} + +func executeECExtensionRemove(p *types.Plan, step *types.PlanStep) error { + in, ok := step.Input.(types.PlanStepInputECExtension) + if !ok { + return errors.New("invalid input for embedded cluster extension remove step") + } + if err := websocket.RemoveExtension(in.Repos, in.Chart, p.AppSlug, p.VersionLabel, step.ID); err != nil { + return errors.Wrap(err, "remove extension") + } + return nil +} + +func getECExtensions(kcli kbclient.Client, newSpec *ecv1beta1.ConfigSpec) (ecv1beta1.Extensions, ecv1beta1.Extensions, error) { + currInstall, err := embeddedcluster.GetCurrentInstallation(context.Background(), kcli) + if err != nil { + return ecv1beta1.Extensions{}, ecv1beta1.Extensions{}, errors.Wrap(err, "get current embedded cluster installation") + } + return currInstall.Spec.Config.Extensions, newSpec.Extensions, nil +} + +func diffECExtensions(oldExts, newExts ecv1beta1.Extensions) ExtensionsDiffResult { + oldCharts := make(map[string]ecv1beta1.Chart) + newCharts := make(map[string]ecv1beta1.Chart) + + if oldExts.Helm != nil { + for _, chart := range oldExts.Helm.Charts { + oldCharts[chart.Name] = chart + } + } + if newExts.Helm != nil { + for _, chart := range newExts.Helm.Charts { + newCharts[chart.Name] = chart + } + } + + var added, removed, modified []ecv1beta1.Chart + + // find removed and modified charts. + for name, oldChart := range oldCharts { + newChart, exists := newCharts[name] + if !exists { + // chart was removed. + removed = append(removed, oldChart) + } else if !reflect.DeepEqual(oldChart, newChart) { + // chart was modified. + modified = append(modified, newChart) + } + } + + // find added charts. + for name, newChart := range newCharts { + if _, exists := oldCharts[name]; !exists { + // chart was added. + added = append(added, newChart) + } + } + + return ExtensionsDiffResult{ + Added: added, + Removed: removed, + Modified: modified, + } +} diff --git a/pkg/plan/plan.go b/pkg/plan/plan.go new file mode 100644 index 0000000000..c97f511a58 --- /dev/null +++ b/pkg/plan/plan.go @@ -0,0 +1,382 @@ +package plan + +import ( + "sync" + "time" + + "github.com/pkg/errors" + "github.com/replicatedhq/kots/pkg/logger" + "github.com/replicatedhq/kots/pkg/plan/types" + "github.com/replicatedhq/kots/pkg/store" + "github.com/replicatedhq/kots/pkg/util" + "github.com/segmentio/ksuid" + kbclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +var planMutex sync.Mutex + +type PlanUpgradeOptions struct { + AppSlug string + VersionLabel string + UpdateCursor string + ChannelID string +} + +func PlanUpgrade(s store.Store, kcli kbclient.Client, opts PlanUpgradeOptions) (*types.Plan, error) { + a, err := s.GetAppFromSlug(opts.AppSlug) + if err != nil { + return nil, errors.Wrap(err, "get app from slug") + } + + manifests, err := getReleaseManifests(a, opts.VersionLabel, opts.ChannelID, opts.UpdateCursor) + if err != nil { + return nil, errors.Wrap(err, "get release manifests") + } + + newECConfigSpec, err := findECConfigSpecInRelease(manifests) + if err != nil { + return nil, errors.Wrap(err, "find embedded cluster config in release") + } + + p := types.Plan{ + ID: ksuid.New().String(), + AppID: a.ID, + AppSlug: opts.AppSlug, + VersionLabel: opts.VersionLabel, + UpdateCursor: opts.UpdateCursor, + ChannelID: opts.ChannelID, + CurrentECVersion: util.EmbeddedClusterVersion(), + NewECVersion: newECConfigSpec.Version, + IsAirgap: a.IsAirgap, + Steps: []*types.PlanStep{}, + } + + // app upgrade service + ausInput, err := getAppUpgradeServiceInput(s, &p, ksuid.New().String()) + if err != nil { + return nil, errors.Wrap(err, "get app upgrade service input") + } + p.Steps = append(p.Steps, &types.PlanStep{ + ID: ausInput.Params.PlanStepID, + Name: "App Upgrade Service", + Type: types.StepTypeAppUpgradeService, + Status: types.StepStatusPending, + StatusDescription: "Pending", + Owner: types.StepOwnerKOTS, + Input: *ausInput, + }) + + // embedded cluster upgrade + requiresECUpgrade, err := requiresECUpgrade(kcli, newECConfigSpec) + if err != nil { + return nil, errors.Wrap(err, "check if requires ec upgrade") + } + if requiresECUpgrade { + in, err := getECUpgradeInput(s, kcli, a, opts.VersionLabel, newECConfigSpec) + if err != nil { + return nil, errors.Wrap(err, "get ec upgrade input") + } + p.Steps = append(p.Steps, &types.PlanStep{ + ID: ksuid.New().String(), + Name: "Embedded Cluster Upgrade", + Type: types.StepTypeECUpgrade, + Status: types.StepStatusPending, + StatusDescription: "Pending embedded cluster upgrade", + Input: *in, + Owner: types.StepOwnerECManager, + }) + } + + // TODO (@salah) implement our EC addons upgrade (have to use EC release metadata?). use same diff logic below + + currECExts, newECExts, err := getECExtensions(kcli, newECConfigSpec) + if err != nil { + return nil, errors.Wrap(err, "get extensions") + } + + ecExtsDiff := diffECExtensions(currECExts, newECExts) + newRepos := newECExts.Helm.Repositories + + // added extensions + for _, chart := range ecExtsDiff.Added { + p.Steps = append(p.Steps, &types.PlanStep{ + ID: ksuid.New().String(), + Name: "Extension Add", + Type: types.StepTypeECExtensionAdd, + Status: types.StepStatusPending, + StatusDescription: "Pending extension addition", + Input: types.PlanStepInputECExtension{ + Repos: newRepos, + Chart: chart, + }, + Owner: types.StepOwnerECManager, + }) + } + + // modified extensions + for _, chart := range ecExtsDiff.Modified { + p.Steps = append(p.Steps, &types.PlanStep{ + ID: ksuid.New().String(), + Name: "Extension Upgrade", + Type: types.StepTypeECExtensionUpgrade, + Status: types.StepStatusPending, + StatusDescription: "Pending extension upgrade", + Input: types.PlanStepInputECExtension{ + Repos: newRepos, + Chart: chart, + }, + Owner: types.StepOwnerECManager, + }) + } + + // removed extensions + for _, chart := range ecExtsDiff.Removed { + p.Steps = append(p.Steps, &types.PlanStep{ + ID: ksuid.New().String(), + Name: "Extension Remove", + Type: types.StepTypeECExtensionRemove, + Status: types.StepStatusPending, + StatusDescription: "Pending extension removal", + Input: types.PlanStepInputECExtension{ + Repos: newRepos, + Chart: chart, + }, + Owner: types.StepOwnerECManager, + }) + } + + // app upgrade + p.Steps = append(p.Steps, &types.PlanStep{ + ID: ksuid.New().String(), + Name: "Application Upgrade", + Type: types.StepTypeAppUpgrade, + Status: types.StepStatusPending, + StatusDescription: "Pending application upgrade", + Owner: types.StepOwnerKOTS, + // the input here is the app upgrade service output + }) + + return &p, nil +} + +func Resume(s store.Store) error { + apps, err := s.ListInstalledApps() + if err != nil { + return errors.Wrap(err, "list installed apps") + } + if len(apps) == 0 { + return nil + } + if len(apps) > 1 { + return errors.New("more than one app is installed") + } + + p, _, err := s.GetCurrentPlan(apps[0].ID) + if err != nil { + return errors.Wrap(err, "get active plan") + } + if p == nil || p.HasEnded() { + return nil + } + + go func() { + if err := Execute(s, p); err != nil { + logger.Error(errors.Wrapf(err, "failed to execute plan %s", p.ID)) + } + }() + + return nil +} + +// TODO (@salah): make each step report better status +func Execute(s store.Store, p *types.Plan) error { + stopCh := make(chan struct{}) + defer close(stopCh) + go startPlanMonitor(s, p, stopCh) + + for _, step := range p.Steps { + if err := executeStep(s, p, step); err != nil { + return errors.Wrap(err, "execute step") + } + } + + return nil +} + +func startPlanMonitor(s store.Store, p *types.Plan, stopCh chan struct{}) { + for { + select { + case <-stopCh: + return + case <-time.After(time.Second * 2): + updated, err := s.GetPlan(p.AppID, p.VersionLabel) + if err != nil { + logger.Error(errors.Wrap(err, "get plan")) + continue + } + *p = *updated + } + } +} + +func executeStep(s store.Store, p *types.Plan, step *types.PlanStep) (finalError error) { + defer func() { + if finalError != nil { + if err := markStepFailed(s, p, step.ID, finalError); err != nil { + logger.Error(errors.Wrap(err, "mark step failed")) + } + } + }() + + switch step.Status { + case types.StepStatusFailed: + return errors.Errorf("step has already failed. status: %q. description: %q", step.Status, step.StatusDescription) + case types.StepStatusComplete: + logger.Infof("Skipping step %q of plan %q because it already completed", step.Name, p.ID) + return nil + } + + logger.Infof("Executing step %q of plan %q", step.Name, p.ID) + + switch step.Type { + case types.StepTypeAppUpgradeService: + if step.Status != types.StepStatusPending { + return errors.Errorf("step %q cannot be resumed", step.Name) + } + if err := executeAppUpgradeService(s, p, step); err != nil { + return errors.Wrap(err, "execute app upgrade service") + } + if err := waitForStep(s, p, step.ID); err != nil { + return errors.Wrap(err, "wait for upgrade service") + } + + case types.StepTypeECUpgrade: + if step.Status == types.StepStatusPending { + if err := executeECUpgrade(s, p, step); err != nil { + return errors.Wrap(err, "execute embedded cluster upgrade") + } + } + if err := waitForStep(s, p, step.ID); err != nil { + return errors.Wrap(err, "wait for embedded cluster upgrade") + } + + case types.StepTypeECExtensionAdd: + if step.Status == types.StepStatusPending { + if err := executeECExtensionAdd(p, step); err != nil { + return errors.Wrap(err, "execute embedded cluster extension add") + } + } + if err := waitForStep(s, p, step.ID); err != nil { + return errors.Wrap(err, "wait for embedded cluster extension add") + } + + case types.StepTypeECExtensionUpgrade: + if step.Status == types.StepStatusPending { + if err := executeECExtensionUpgrade(p, step); err != nil { + return errors.Wrap(err, "execute embedded cluster extension upgrade") + } + } + if err := waitForStep(s, p, step.ID); err != nil { + return errors.Wrap(err, "wait for embedded cluster extension upgrade") + } + + case types.StepTypeECExtensionRemove: + if step.Status == types.StepStatusPending { + if err := executeECExtensionRemove(p, step); err != nil { + return errors.Wrap(err, "execute embedded cluster extension remove") + } + } + if err := waitForStep(s, p, step.ID); err != nil { + return errors.Wrap(err, "wait for embedded cluster extension remove") + } + + case types.StepTypeAppUpgrade: + if err := executeAppUpgrade(s, p, step); err != nil { + return errors.Wrap(err, "execute app upgrade") + } + default: + return errors.Errorf("unknown step type %q", step.Type) + } + + logger.Infof("Step %q of plan %q completed", step.Name, p.ID) + return nil +} + +func waitForStep(s store.Store, p *types.Plan, stepID string) error { + for { + stepIndex := -1 + for i, step := range p.Steps { + if step.ID == stepID { + stepIndex = i + break + } + } + if stepIndex == -1 { + return errors.Errorf("step %s not found in plan %s", stepID, p.ID) + } + + if p.Steps[stepIndex].Status == types.StepStatusComplete { + return nil + } + if p.Steps[stepIndex].Status == types.StepStatusFailed { + return errors.Errorf("step failed: %s", p.Steps[stepIndex].StatusDescription) + } + + time.Sleep(time.Second * 2) + } +} + +func markStepFailed(s store.Store, p *types.Plan, stepID string, err error) error { + return UpdateStep(s, UpdateStepOptions{ + AppSlug: p.AppSlug, + VersionLabel: p.VersionLabel, + StepID: stepID, + Status: types.StepStatusFailed, + StatusDescription: err.Error(), + }) +} + +type UpdateStepOptions struct { + AppSlug string + VersionLabel string + StepID string + Status types.PlanStepStatus + StatusDescription string + Output string +} + +func UpdateStep(s store.Store, opts UpdateStepOptions) error { + planMutex.Lock() + defer planMutex.Unlock() + + a, err := s.GetAppFromSlug(opts.AppSlug) + if err != nil { + return errors.Wrap(err, "get app from slug") + } + + p, err := s.GetPlan(a.ID, opts.VersionLabel) + if err != nil { + return errors.Wrap(err, "get plan") + } + + stepIndex := -1 + for i, s := range p.Steps { + if s.ID == opts.StepID { + stepIndex = i + break + } + } + if stepIndex == -1 { + return errors.Errorf("step %s not found in plan", opts.StepID) + } + + p.Steps[stepIndex].Status = opts.Status + p.Steps[stepIndex].StatusDescription = opts.StatusDescription + p.Steps[stepIndex].Output = opts.Output + + if err := s.UpsertPlan(p); err != nil { + return errors.Wrap(err, "update plan") + } + + return nil +} diff --git a/pkg/plan/types/types.go b/pkg/plan/types/types.go new file mode 100644 index 0000000000..e1aec016d6 --- /dev/null +++ b/pkg/plan/types/types.go @@ -0,0 +1,118 @@ +package types + +import ( + k0sv1beta1 "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + upgradeservicetypes "github.com/replicatedhq/kots/pkg/upgradeservice/types" + kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" +) + +type Plan struct { + ID string `json:"id" yaml:"id"` + AppID string `json:"appId" yaml:"appId"` + AppSlug string `json:"appSlug" yaml:"appSlug"` + VersionLabel string `json:"versionLabel" yaml:"versionLabel"` + UpdateCursor string `json:"updateCursor" yaml:"updateCursor"` + ChannelID string `json:"channelId" yaml:"channelId"` + CurrentECVersion string `json:"currentECVersion" yaml:"currentECVersion"` + NewECVersion string `json:"newECVersion" yaml:"newECVersion"` + IsAirgap bool `json:"isAirgap" yaml:"isAirgap"` + BaseSequence int64 `json:"baseSequence" yaml:"baseSequence"` + NextSequence int64 `json:"nextSequence" yaml:"nextSequence"` + Source string `json:"source" yaml:"source"` + Steps []*PlanStep `json:"steps" yaml:"steps"` +} + +type PlanStep struct { + ID string `json:"id" yaml:"id"` + Name string `json:"name" yaml:"name"` + Type PlanStepType `json:"type" yaml:"type"` + Status PlanStepStatus `json:"status" yaml:"status"` + StatusDescription string `json:"statusDescription" yaml:"statusDescription"` + Owner PlanStepOwner `json:"owner" yaml:"owner"` + OwnerHost string `json:"ownerHost" yaml:"ownerHost"` + Input interface{} `json:"input" yaml:"input"` + Output interface{} `json:"output" yaml:"output"` +} + +type PlanStepType string + +const ( + StepTypeAppUpgradeService PlanStepType = "app-upgrade-service" + StepTypeAppUpgrade PlanStepType = "app-upgrade" + StepTypeECUpgrade PlanStepType = "ec-upgrade" + StepTypeECExtensionAdd PlanStepType = "ec-extension-add" + StepTypeECExtensionUpgrade PlanStepType = "ec-extension-upgrade" + StepTypeECExtensionRemove PlanStepType = "ec-extension-remove" +) + +type PlanStepStatus string + +const ( + StepStatusPending PlanStepStatus = "pending" + StepStatusStarting PlanStepStatus = "starting" + StepStatusRunning PlanStepStatus = "running" + StepStatusComplete PlanStepStatus = "complete" + StepStatusFailed PlanStepStatus = "failed" +) + +type PlanStepOwner string + +const ( + StepOwnerKOTS PlanStepOwner = "kots" + StepOwnerECManager PlanStepOwner = "manager" +) + +type PlanStepInputAppUpgradeService struct { + Params upgradeservicetypes.UpgradeServiceParams `json:"params" yaml:"params"` +} + +type PlanStepInputECUpgrade struct { + CurrentECInstallation ecv1beta1.Installation `json:"currentECInstallation" yaml:"currentECInstallation"` + CurrentKOTSInstallation kotsv1beta1.Installation `json:"currentKOTSInstallation" yaml:"currentKOTSInstallation"` + NewECConfigSpec ecv1beta1.ConfigSpec `json:"newECConfigSpec" yaml:"newECConfigSpec"` + IsDisasterRecoverySupported bool `json:"isDisasterRecoverySupported" yaml:"isDisasterRecoverySupported"` +} + +type PlanStepInputECExtension struct { + Repos []k0sv1beta1.Repository `json:"repos" yaml:"repos"` + Chart ecv1beta1.Chart `json:"chart" yaml:"chart"` +} + +func (p *Plan) HasEnded() bool { + status := p.GetStatus() + return status == StepStatusFailed || status == StepStatusComplete +} + +func (p *Plan) GetStatus() PlanStepStatus { + return p.CurrentStep().Status +} + +func (p *Plan) CurrentStep() *PlanStep { + for _, s := range p.Steps { + if s.Status == StepStatusFailed { + return s + } + } + for _, s := range p.Steps { + if s.Status == StepStatusStarting { + return s + } + } + for _, s := range p.Steps { + if s.Status == StepStatusRunning { + return s + } + } + for _, s := range p.Steps { + if s.Status == StepStatusPending { + return s + } + } + for _, s := range p.Steps { + if s.Status == StepStatusComplete { + return s + } + } + return &PlanStep{} +} diff --git a/pkg/plan/util.go b/pkg/plan/util.go new file mode 100644 index 0000000000..e09584fb9f --- /dev/null +++ b/pkg/plan/util.go @@ -0,0 +1,192 @@ +package plan + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + + "github.com/pkg/errors" + apptypes "github.com/replicatedhq/kots/pkg/app/types" + "github.com/replicatedhq/kots/pkg/archives" + "github.com/replicatedhq/kots/pkg/filestore" + "github.com/replicatedhq/kots/pkg/kotsutil" + "github.com/replicatedhq/kots/pkg/plan/types" + "github.com/replicatedhq/kots/pkg/replicatedapp" + "github.com/replicatedhq/kots/pkg/reporting" + "github.com/replicatedhq/kots/pkg/update" + "github.com/replicatedhq/kots/pkg/util" +) + +func getReleaseManifests(a *apptypes.App, versionLabel, channelID, updateCursor string) (map[string][]byte, error) { + if a.IsAirgap { + return getAppManifestsFromAirgap(a, versionLabel, channelID, updateCursor) + } + return getAppManifestsFromOnline(a, versionLabel, channelID, updateCursor) +} + +func getAppManifestsFromAirgap(a *apptypes.App, versionLabel, channelID, updateCursor string) (map[string][]byte, error) { + manifests := make(map[string][]byte) + + airgapArchive, err := update.GetAirgapUpdate(a.Slug, channelID, updateCursor) + if err != nil { + return nil, errors.Wrap(err, "get airgap update") + } + + appArchive, err := archives.GetFileContentFromTGZArchive("app.tar.gz", airgapArchive) + if err != nil { + return nil, errors.Wrap(err, "extract app archive") + } + + tempDir, err := os.MkdirTemp("", "kotsadm") + if err != nil { + return nil, errors.Wrap(err, "create temp dir") + } + defer os.RemoveAll(tempDir) + + if err := archives.ExtractTGZArchiveFromReader(bytes.NewReader(appArchive), tempDir); err != nil { + return nil, errors.Wrap(err, "extract app archive") + } + + err = filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + return nil + } + + contents, err := os.ReadFile(path) + if err != nil { + return errors.Wrap(err, "read file") + } + + manifests[path] = contents + return nil + }) + if err != nil { + return nil, errors.Wrap(err, "walk temp dir") + } + + return manifests, nil +} + +func getAppManifestsFromOnline(a *apptypes.App, versionLabel, channelID, updateCursor string) (map[string][]byte, error) { + manifests := make(map[string][]byte) + + u, err := url.ParseRequestURI(fmt.Sprintf("replicated://%s", a.Slug)) + if err != nil { + return nil, errors.Wrap(err, "parse request uri failed") + } + + replicatedUpstream, err := replicatedapp.ParseReplicatedURL(u) + if err != nil { + return nil, errors.Wrap(err, "parse replicated upstream") + } + + license, err := kotsutil.LoadLicenseFromBytes([]byte(a.License)) + if err != nil { + return nil, errors.Wrap(err, "parse app license") + } + + getReq, err := replicatedUpstream.GetRequest("GET", license, updateCursor, channelID) + if err != nil { + return nil, errors.Wrap(err, "create http request") + } + + reporting.InjectReportingInfoHeaders(getReq, reporting.GetReportingInfo(a.ID)) + + getResp, err := http.DefaultClient.Do(getReq) + if err != nil { + return nil, errors.Wrap(err, "execute get request") + } + defer getResp.Body.Close() + + if getResp.StatusCode >= 300 { + body, _ := io.ReadAll(getResp.Body) + if len(body) > 0 { + return nil, util.ActionableError{Message: string(body)} + } + return nil, errors.Errorf("unexpected result from get request: %d", getResp.StatusCode) + } + + gzipReader, err := gzip.NewReader(getResp.Body) + if err != nil { + return nil, errors.Wrap(err, "create new gzip reader") + } + + tarReader := tar.NewReader(gzipReader) + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, errors.Wrap(err, "get next file from reader") + } + + switch header.Typeflag { + case tar.TypeDir: + continue + case tar.TypeReg: + contents, err := io.ReadAll(tarReader) + if err != nil { + return nil, errors.Wrap(err, "read file from tar") + } + manifests[header.Name] = contents + } + } + + return manifests, nil +} + +func getAppUpgradeServiceOutput(p *types.Plan) (map[string]string, error) { + var ausOutput map[string]string + for _, s := range p.Steps { + if s.Type != types.StepTypeAppUpgradeService { + continue + } + output := s.Output.(string) + if output == "" { + return nil, errors.New("app upgrade service step output not found") + } + if err := json.Unmarshal([]byte(output), &ausOutput); err != nil { + return nil, errors.Wrap(err, "unmarshal app upgrade service step output") + } + break + } + if ausOutput == nil { + return nil, errors.New("app upgrade service step output not found") + } + return ausOutput, nil +} + +func getAppArchive(path string) (string, error) { + if path == "" { + return "", errors.New("path is empty") + } + + tgzArchive, err := filestore.GetStore().ReadArchive(path) + if err != nil { + return "", errors.Wrap(err, "read archive") + } + defer os.RemoveAll(tgzArchive) + + archiveDir, err := os.MkdirTemp("", "kotsadm") + if err != nil { + return "", errors.Wrap(err, "create temp dir") + } + + if err := util.ExtractTGZArchive(tgzArchive, archiveDir); err != nil { + return "", errors.Wrap(err, "extract app archive") + } + + return archiveDir, nil +} diff --git a/pkg/replicatedapp/api_test.go b/pkg/replicatedapp/api_test.go index a12b63e070..8072826efb 100644 --- a/pkg/replicatedapp/api_test.go +++ b/pkg/replicatedapp/api_test.go @@ -78,7 +78,7 @@ func Test_getRequest(t *testing.T) { if test.channel != nil { cursor.ChannelName = *test.channel } - request, err := r.GetRequest("GET", license, cursor, channel) + request, err := r.GetRequest("GET", license, cursor.Cursor, channel) req.NoError(err) assert.Equal(t, test.expectedURL, request.URL.String()) } diff --git a/pkg/replicatedapp/upstream.go b/pkg/replicatedapp/upstream.go index 4f52d6ca25..e8e8356c09 100644 --- a/pkg/replicatedapp/upstream.go +++ b/pkg/replicatedapp/upstream.go @@ -41,7 +41,7 @@ func ParseReplicatedURL(u *url.URL) (*ReplicatedUpstream, error) { return &replicatedUpstream, nil } -func (r *ReplicatedUpstream) GetRequest(method string, license *kotsv1beta1.License, cursor ReplicatedCursor, selectedChannelID string) (*http.Request, error) { +func (r *ReplicatedUpstream) GetRequest(method string, license *kotsv1beta1.License, cursor string, selectedChannelID string) (*http.Request, error) { u, err := url.Parse(license.Spec.Endpoint) if err != nil { return nil, errors.Wrap(err, "failed to parse endpoint from license") @@ -58,7 +58,7 @@ func (r *ReplicatedUpstream) GetRequest(method string, license *kotsv1beta1.Lice } urlValues := url.Values{} - urlValues.Set("channelSequence", cursor.Cursor) + urlValues.Set("channelSequence", cursor) if r.VersionLabel != nil { urlValues.Set("versionLabel", *r.VersionLabel) } diff --git a/pkg/store/kotsstore/plan_store.go b/pkg/store/kotsstore/plan_store.go new file mode 100644 index 0000000000..76ae57eea6 --- /dev/null +++ b/pkg/store/kotsstore/plan_store.go @@ -0,0 +1,103 @@ +package kotsstore + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/pkg/errors" + "github.com/replicatedhq/kots/pkg/persistence" + "github.com/replicatedhq/kots/pkg/plan/types" + "github.com/rqlite/gorqlite" +) + +func (s *KOTSStore) GetPlan(appID, versionLabel string) (*types.Plan, error) { + db := persistence.MustGetDBSession() + query := `SELECT plan FROM plan WHERE app_id = ? AND version_label = ?` + rows, err := db.QueryOneParameterized(gorqlite.ParameterizedStatement{ + Query: query, + Arguments: []interface{}{appID, versionLabel}, + }) + if err != nil { + return nil, fmt.Errorf("query: %v: %v", err, rows.Err) + } + if !rows.Next() { + return nil, ErrNotFound + } + + var marshalled string + if err := rows.Scan(&marshalled); err != nil { + return nil, fmt.Errorf("scan: %v", err) + } + + var plan *types.Plan + if err := json.Unmarshal([]byte(marshalled), &plan); err != nil { + return nil, errors.Wrap(err, "unmarshal") + } + + return plan, nil +} + +func (s *KOTSStore) UpsertPlan(p *types.Plan) error { + db := persistence.MustGetDBSession() + + query := ` + INSERT INTO plan (app_id, version_label, created_at, updated_at, status, plan) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT (app_id, version_label) + DO UPDATE SET + updated_at = excluded.updated_at, + status = excluded.status, + plan = excluded.plan + ` + + marshalled, err := json.Marshal(p) + if err != nil { + return errors.Wrap(err, "marshal") + } + + wr, err := db.WriteOneParameterized(gorqlite.ParameterizedStatement{ + Query: query, + Arguments: []interface{}{ + p.AppID, + p.VersionLabel, + time.Now().Unix(), + time.Now().Unix(), + p.GetStatus(), + string(marshalled), + }, + }) + if err != nil { + return fmt.Errorf("write: %v: %v", err, wr.Err) + } + + return err +} + +func (s *KOTSStore) GetCurrentPlan(appID string) (*types.Plan, *time.Time, error) { + db := persistence.MustGetDBSession() + query := `SELECT plan, updated_at FROM plan WHERE app_id = ? ORDER BY updated_at DESC LIMIT 1` + rows, err := db.QueryOneParameterized(gorqlite.ParameterizedStatement{ + Query: query, + Arguments: []interface{}{appID}, + }) + if err != nil { + return nil, nil, fmt.Errorf("query: %v: %v", err, rows.Err) + } + if !rows.Next() { + return nil, nil, nil + } + + var marshalled string + var updatedAt time.Time + if err := rows.Scan(&marshalled, &updatedAt); err != nil { + return nil, nil, errors.Wrap(err, "scan") + } + + var plan *types.Plan + if err := json.Unmarshal([]byte(marshalled), &plan); err != nil { + return nil, nil, errors.Wrap(err, "unmarshal") + } + + return plan, &updatedAt, nil +} diff --git a/pkg/store/mock/mock.go b/pkg/store/mock/mock.go index 4a69bffc71..d02f372752 100644 --- a/pkg/store/mock/mock.go +++ b/pkg/store/mock/mock.go @@ -19,14 +19,15 @@ import ( types4 "github.com/replicatedhq/kots/pkg/appstate/types" types5 "github.com/replicatedhq/kots/pkg/kotsadmsnapshot/types" types6 "github.com/replicatedhq/kots/pkg/online/types" - types7 "github.com/replicatedhq/kots/pkg/preflight/types" - types8 "github.com/replicatedhq/kots/pkg/registry/types" - types9 "github.com/replicatedhq/kots/pkg/render/types" - types10 "github.com/replicatedhq/kots/pkg/session/types" - types11 "github.com/replicatedhq/kots/pkg/store/types" - types12 "github.com/replicatedhq/kots/pkg/supportbundle/types" - types13 "github.com/replicatedhq/kots/pkg/upstream/types" - types14 "github.com/replicatedhq/kots/pkg/user/types" + types7 "github.com/replicatedhq/kots/pkg/plan/types" + types8 "github.com/replicatedhq/kots/pkg/preflight/types" + types9 "github.com/replicatedhq/kots/pkg/registry/types" + types10 "github.com/replicatedhq/kots/pkg/render/types" + types11 "github.com/replicatedhq/kots/pkg/session/types" + types12 "github.com/replicatedhq/kots/pkg/store/types" + types13 "github.com/replicatedhq/kots/pkg/supportbundle/types" + types14 "github.com/replicatedhq/kots/pkg/upstream/types" + types15 "github.com/replicatedhq/kots/pkg/user/types" v1beta10 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1" redact "github.com/replicatedhq/troubleshoot/pkg/redact" ) @@ -112,7 +113,7 @@ func (mr *MockStoreMockRecorder) CreateApp(name, channelID, upstreamURI, license } // CreateAppVersion mocks base method. -func (m *MockStore) CreateAppVersion(appID string, baseSequence *int64, filesInDir, source string, isInstall, isAutomated bool, configFile string, skipPreflights bool, renderer types9.Renderer) (int64, error) { +func (m *MockStore) CreateAppVersion(appID string, baseSequence *int64, filesInDir, source string, isInstall, isAutomated bool, configFile string, skipPreflights bool, renderer types10.Renderer) (int64, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateAppVersion", appID, baseSequence, filesInDir, source, isInstall, isAutomated, configFile, skipPreflights, renderer) ret0, _ := ret[0].(int64) @@ -127,7 +128,7 @@ func (mr *MockStoreMockRecorder) CreateAppVersion(appID, baseSequence, filesInDi } // CreateInProgressSupportBundle mocks base method. -func (m *MockStore) CreateInProgressSupportBundle(supportBundle *types12.SupportBundle) error { +func (m *MockStore) CreateInProgressSupportBundle(supportBundle *types13.SupportBundle) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateInProgressSupportBundle", supportBundle) ret0, _ := ret[0].(error) @@ -171,7 +172,7 @@ func (mr *MockStoreMockRecorder) CreateNewCluster(userID, isAllUsers, title, tok } // CreatePendingDownloadAppVersion mocks base method. -func (m *MockStore) CreatePendingDownloadAppVersion(appID string, update types13.Update, kotsApplication *v1beta10.Application, license *v1beta10.License) (int64, error) { +func (m *MockStore) CreatePendingDownloadAppVersion(appID string, update types14.Update, kotsApplication *v1beta10.Application, license *v1beta10.License) (int64, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreatePendingDownloadAppVersion", appID, update, kotsApplication, license) ret0, _ := ret[0].(int64) @@ -214,10 +215,10 @@ func (mr *MockStoreMockRecorder) CreateScheduledSnapshot(snapshotID, appID, time } // CreateSession mocks base method. -func (m *MockStore) CreateSession(user *types14.User, issuedAt, expiresAt time.Time, roles []string) (*types10.Session, error) { +func (m *MockStore) CreateSession(user *types15.User, issuedAt, expiresAt time.Time, roles []string) (*types11.Session, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateSession", user, issuedAt, expiresAt, roles) - ret0, _ := ret[0].(*types10.Session) + ret0, _ := ret[0].(*types11.Session) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -229,10 +230,10 @@ func (mr *MockStoreMockRecorder) CreateSession(user, issuedAt, expiresAt, roles } // CreateSupportBundle mocks base method. -func (m *MockStore) CreateSupportBundle(bundleID, appID, archivePath string, marshalledTree []byte) (*types12.SupportBundle, error) { +func (m *MockStore) CreateSupportBundle(bundleID, appID, archivePath string, marshalledTree []byte) (*types13.SupportBundle, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateSupportBundle", bundleID, appID, archivePath, marshalledTree) - ret0, _ := ret[0].(*types12.SupportBundle) + ret0, _ := ret[0].(*types13.SupportBundle) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -610,6 +611,22 @@ func (mr *MockStoreMockRecorder) GetCurrentParentSequence(appID, clusterID inter return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentParentSequence", reflect.TypeOf((*MockStore)(nil).GetCurrentParentSequence), appID, clusterID) } +// GetCurrentPlan mocks base method. +func (m *MockStore) GetCurrentPlan(appID string) (*types7.Plan, *time.Time, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCurrentPlan", appID) + ret0, _ := ret[0].(*types7.Plan) + ret1, _ := ret[1].(*time.Time) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetCurrentPlan indicates an expected call of GetCurrentPlan. +func (mr *MockStoreMockRecorder) GetCurrentPlan(appID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentPlan", reflect.TypeOf((*MockStore)(nil).GetCurrentPlan), appID) +} + // GetCurrentUpdateCursor mocks base method. func (m *MockStore) GetCurrentUpdateCursor(appID, channelID string) (string, error) { m.ctrl.T.Helper() @@ -686,10 +703,10 @@ func (mr *MockStoreMockRecorder) GetDownstreamVersionSource(appID, sequence inte } // GetDownstreamVersionStatus mocks base method. -func (m *MockStore) GetDownstreamVersionStatus(appID string, sequence int64) (types11.DownstreamVersionStatus, error) { +func (m *MockStore) GetDownstreamVersionStatus(appID string, sequence int64) (types12.DownstreamVersionStatus, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetDownstreamVersionStatus", appID, sequence) - ret0, _ := ret[0].(types11.DownstreamVersionStatus) + ret0, _ := ret[0].(types12.DownstreamVersionStatus) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -957,6 +974,21 @@ func (mr *MockStoreMockRecorder) GetPendingInstallationStatus() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPendingInstallationStatus", reflect.TypeOf((*MockStore)(nil).GetPendingInstallationStatus)) } +// GetPlan mocks base method. +func (m *MockStore) GetPlan(appID, versionLabel string) (*types7.Plan, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPlan", appID, versionLabel) + ret0, _ := ret[0].(*types7.Plan) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPlan indicates an expected call of GetPlan. +func (mr *MockStoreMockRecorder) GetPlan(appID, versionLabel interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPlan", reflect.TypeOf((*MockStore)(nil).GetPlan), appID, versionLabel) +} + // GetPreflightProgress mocks base method. func (m *MockStore) GetPreflightProgress(appID string, sequence int64) (string, error) { m.ctrl.T.Helper() @@ -973,10 +1005,10 @@ func (mr *MockStoreMockRecorder) GetPreflightProgress(appID, sequence interface{ } // GetPreflightResults mocks base method. -func (m *MockStore) GetPreflightResults(appID string, sequence int64) (*types7.PreflightResult, error) { +func (m *MockStore) GetPreflightResults(appID string, sequence int64) (*types8.PreflightResult, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetPreflightResults", appID, sequence) - ret0, _ := ret[0].(*types7.PreflightResult) + ret0, _ := ret[0].(*types8.PreflightResult) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1033,10 +1065,10 @@ func (mr *MockStoreMockRecorder) GetRedactions(bundleID interface{}) *gomock.Cal } // GetRegistryDetailsForApp mocks base method. -func (m *MockStore) GetRegistryDetailsForApp(appID string) (types8.RegistrySettings, error) { +func (m *MockStore) GetRegistryDetailsForApp(appID string) (types9.RegistrySettings, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetRegistryDetailsForApp", appID) - ret0, _ := ret[0].(types8.RegistrySettings) + ret0, _ := ret[0].(types9.RegistrySettings) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1048,10 +1080,10 @@ func (mr *MockStoreMockRecorder) GetRegistryDetailsForApp(appID interface{}) *go } // GetSession mocks base method. -func (m *MockStore) GetSession(sessionID string) (*types10.Session, error) { +func (m *MockStore) GetSession(sessionID string) (*types11.Session, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSession", sessionID) - ret0, _ := ret[0].(*types10.Session) + ret0, _ := ret[0].(*types11.Session) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1078,10 +1110,10 @@ func (mr *MockStoreMockRecorder) GetSharedPasswordBcrypt() *gomock.Call { } // GetStatusForVersion mocks base method. -func (m *MockStore) GetStatusForVersion(appID, clusterID string, sequence int64) (types11.DownstreamVersionStatus, error) { +func (m *MockStore) GetStatusForVersion(appID, clusterID string, sequence int64) (types12.DownstreamVersionStatus, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetStatusForVersion", appID, clusterID, sequence) - ret0, _ := ret[0].(types11.DownstreamVersionStatus) + ret0, _ := ret[0].(types12.DownstreamVersionStatus) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1093,10 +1125,10 @@ func (mr *MockStoreMockRecorder) GetStatusForVersion(appID, clusterID, sequence } // GetSupportBundle mocks base method. -func (m *MockStore) GetSupportBundle(bundleID string) (*types12.SupportBundle, error) { +func (m *MockStore) GetSupportBundle(bundleID string) (*types13.SupportBundle, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSupportBundle", bundleID) - ret0, _ := ret[0].(*types12.SupportBundle) + ret0, _ := ret[0].(*types13.SupportBundle) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1108,10 +1140,10 @@ func (mr *MockStoreMockRecorder) GetSupportBundle(bundleID interface{}) *gomock. } // GetSupportBundleAnalysis mocks base method. -func (m *MockStore) GetSupportBundleAnalysis(bundleID string) (*types12.SupportBundleAnalysis, error) { +func (m *MockStore) GetSupportBundleAnalysis(bundleID string) (*types13.SupportBundleAnalysis, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSupportBundleAnalysis", bundleID) - ret0, _ := ret[0].(*types12.SupportBundleAnalysis) + ret0, _ := ret[0].(*types13.SupportBundleAnalysis) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1287,7 +1319,7 @@ func (mr *MockStoreMockRecorder) IsRollbackSupportedForVersion(appID, sequence i } // IsSnapshotsSupportedForVersion mocks base method. -func (m *MockStore) IsSnapshotsSupportedForVersion(a *types3.App, sequence int64, renderer types9.Renderer) (bool, error) { +func (m *MockStore) IsSnapshotsSupportedForVersion(a *types3.App, sequence int64, renderer types10.Renderer) (bool, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "IsSnapshotsSupportedForVersion", a, sequence, renderer) ret0, _ := ret[0].(bool) @@ -1422,10 +1454,10 @@ func (mr *MockStoreMockRecorder) ListPendingScheduledSnapshots(appID interface{} } // ListSupportBundles mocks base method. -func (m *MockStore) ListSupportBundles(appID string) ([]*types12.SupportBundle, error) { +func (m *MockStore) ListSupportBundles(appID string) ([]*types13.SupportBundle, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListSupportBundles", appID) - ret0, _ := ret[0].([]*types12.SupportBundle) + ret0, _ := ret[0].([]*types13.SupportBundle) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1589,7 +1621,7 @@ func (mr *MockStoreMockRecorder) SetAutoDeploy(appID, autoDeploy interface{}) *g } // SetDownstreamVersionStatus mocks base method. -func (m *MockStore) SetDownstreamVersionStatus(appID string, sequence int64, status types11.DownstreamVersionStatus, statusInfo string) error { +func (m *MockStore) SetDownstreamVersionStatus(appID string, sequence int64, status types12.DownstreamVersionStatus, statusInfo string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SetDownstreamVersionStatus", appID, sequence, status, statusInfo) ret0, _ := ret[0].(error) @@ -1800,7 +1832,7 @@ func (mr *MockStoreMockRecorder) SetUpdateCheckerSpec(appID, updateCheckerSpec i } // UpdateAppLicense mocks base method. -func (m *MockStore) UpdateAppLicense(appID string, sequence int64, archiveDir string, newLicense *v1beta10.License, originalLicenseData string, channelChanged, failOnVersionCreate bool, renderer types9.Renderer, reportingInfo *types1.ReportingInfo) (int64, error) { +func (m *MockStore) UpdateAppLicense(appID string, sequence int64, archiveDir string, newLicense *v1beta10.License, originalLicenseData string, channelChanged, failOnVersionCreate bool, renderer types10.Renderer, reportingInfo *types1.ReportingInfo) (int64, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateAppLicense", appID, sequence, archiveDir, newLicense, originalLicenseData, channelChanged, failOnVersionCreate, renderer, reportingInfo) ret0, _ := ret[0].(int64) @@ -1829,7 +1861,7 @@ func (mr *MockStoreMockRecorder) UpdateAppLicenseSyncNow(appID interface{}) *gom } // UpdateAppVersion mocks base method. -func (m *MockStore) UpdateAppVersion(appID string, sequence int64, baseSequence *int64, filesInDir, source string, skipPreflights bool, renderer types9.Renderer) error { +func (m *MockStore) UpdateAppVersion(appID string, sequence int64, baseSequence *int64, filesInDir, source string, skipPreflights bool, renderer types10.Renderer) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateAppVersion", appID, sequence, baseSequence, filesInDir, source, skipPreflights, renderer) ret0, _ := ret[0].(error) @@ -1927,7 +1959,7 @@ func (mr *MockStoreMockRecorder) UpdateSessionExpiresAt(sessionID, expiresAt int } // UpdateSupportBundle mocks base method. -func (m *MockStore) UpdateSupportBundle(bundle *types12.SupportBundle) error { +func (m *MockStore) UpdateSupportBundle(bundle *types13.SupportBundle) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateSupportBundle", bundle) ret0, _ := ret[0].(error) @@ -1954,6 +1986,20 @@ func (mr *MockStoreMockRecorder) UploadSupportBundle(bundleID, archivePath, mars return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadSupportBundle", reflect.TypeOf((*MockStore)(nil).UploadSupportBundle), bundleID, archivePath, marshalledTree) } +// UpsertPlan mocks base method. +func (m *MockStore) UpsertPlan(plan *types7.Plan) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpsertPlan", plan) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpsertPlan indicates an expected call of UpsertPlan. +func (mr *MockStoreMockRecorder) UpsertPlan(plan interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertPlan", reflect.TypeOf((*MockStore)(nil).UpsertPlan), plan) +} + // WaitForReady mocks base method. func (m *MockStore) WaitForReady(ctx context.Context) error { m.ctrl.T.Helper() @@ -2042,10 +2088,10 @@ func (mr *MockRegistryStoreMockRecorder) GetAppIDsFromRegistry(hostname interfac } // GetRegistryDetailsForApp mocks base method. -func (m *MockRegistryStore) GetRegistryDetailsForApp(appID string) (types8.RegistrySettings, error) { +func (m *MockRegistryStore) GetRegistryDetailsForApp(appID string) (types9.RegistrySettings, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetRegistryDetailsForApp", appID) - ret0, _ := ret[0].(types8.RegistrySettings) + ret0, _ := ret[0].(types9.RegistrySettings) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -2094,7 +2140,7 @@ func (m *MockSupportBundleStore) EXPECT() *MockSupportBundleStoreMockRecorder { } // CreateInProgressSupportBundle mocks base method. -func (m *MockSupportBundleStore) CreateInProgressSupportBundle(supportBundle *types12.SupportBundle) error { +func (m *MockSupportBundleStore) CreateInProgressSupportBundle(supportBundle *types13.SupportBundle) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateInProgressSupportBundle", supportBundle) ret0, _ := ret[0].(error) @@ -2108,10 +2154,10 @@ func (mr *MockSupportBundleStoreMockRecorder) CreateInProgressSupportBundle(supp } // CreateSupportBundle mocks base method. -func (m *MockSupportBundleStore) CreateSupportBundle(bundleID, appID, archivePath string, marshalledTree []byte) (*types12.SupportBundle, error) { +func (m *MockSupportBundleStore) CreateSupportBundle(bundleID, appID, archivePath string, marshalledTree []byte) (*types13.SupportBundle, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateSupportBundle", bundleID, appID, archivePath, marshalledTree) - ret0, _ := ret[0].(*types12.SupportBundle) + ret0, _ := ret[0].(*types13.SupportBundle) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -2152,10 +2198,10 @@ func (mr *MockSupportBundleStoreMockRecorder) GetRedactions(bundleID interface{} } // GetSupportBundle mocks base method. -func (m *MockSupportBundleStore) GetSupportBundle(bundleID string) (*types12.SupportBundle, error) { +func (m *MockSupportBundleStore) GetSupportBundle(bundleID string) (*types13.SupportBundle, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSupportBundle", bundleID) - ret0, _ := ret[0].(*types12.SupportBundle) + ret0, _ := ret[0].(*types13.SupportBundle) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -2167,10 +2213,10 @@ func (mr *MockSupportBundleStoreMockRecorder) GetSupportBundle(bundleID interfac } // GetSupportBundleAnalysis mocks base method. -func (m *MockSupportBundleStore) GetSupportBundleAnalysis(bundleID string) (*types12.SupportBundleAnalysis, error) { +func (m *MockSupportBundleStore) GetSupportBundleAnalysis(bundleID string) (*types13.SupportBundleAnalysis, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSupportBundleAnalysis", bundleID) - ret0, _ := ret[0].(*types12.SupportBundleAnalysis) + ret0, _ := ret[0].(*types13.SupportBundleAnalysis) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -2197,10 +2243,10 @@ func (mr *MockSupportBundleStoreMockRecorder) GetSupportBundleArchive(bundleID i } // ListSupportBundles mocks base method. -func (m *MockSupportBundleStore) ListSupportBundles(appID string) ([]*types12.SupportBundle, error) { +func (m *MockSupportBundleStore) ListSupportBundles(appID string) ([]*types13.SupportBundle, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListSupportBundles", appID) - ret0, _ := ret[0].([]*types12.SupportBundle) + ret0, _ := ret[0].([]*types13.SupportBundle) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -2240,7 +2286,7 @@ func (mr *MockSupportBundleStoreMockRecorder) SetSupportBundleAnalysis(bundleID, } // UpdateSupportBundle mocks base method. -func (m *MockSupportBundleStore) UpdateSupportBundle(bundle *types12.SupportBundle) error { +func (m *MockSupportBundleStore) UpdateSupportBundle(bundle *types13.SupportBundle) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateSupportBundle", bundle) ret0, _ := ret[0].(error) @@ -2306,10 +2352,10 @@ func (mr *MockPreflightStoreMockRecorder) GetPreflightProgress(appID, sequence i } // GetPreflightResults mocks base method. -func (m *MockPreflightStore) GetPreflightResults(appID string, sequence int64) (*types7.PreflightResult, error) { +func (m *MockPreflightStore) GetPreflightResults(appID string, sequence int64) (*types8.PreflightResult, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetPreflightResults", appID, sequence) - ret0, _ := ret[0].(*types7.PreflightResult) + ret0, _ := ret[0].(*types8.PreflightResult) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -2533,10 +2579,10 @@ func (m *MockSessionStore) EXPECT() *MockSessionStoreMockRecorder { } // CreateSession mocks base method. -func (m *MockSessionStore) CreateSession(user *types14.User, issuedAt, expiresAt time.Time, roles []string) (*types10.Session, error) { +func (m *MockSessionStore) CreateSession(user *types15.User, issuedAt, expiresAt time.Time, roles []string) (*types11.Session, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateSession", user, issuedAt, expiresAt, roles) - ret0, _ := ret[0].(*types10.Session) + ret0, _ := ret[0].(*types11.Session) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -2576,10 +2622,10 @@ func (mr *MockSessionStoreMockRecorder) DeleteSession(sessionID interface{}) *go } // GetSession mocks base method. -func (m *MockSessionStore) GetSession(sessionID string) (*types10.Session, error) { +func (m *MockSessionStore) GetSession(sessionID string) (*types11.Session, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSession", sessionID) - ret0, _ := ret[0].(*types10.Session) + ret0, _ := ret[0].(*types11.Session) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -3141,10 +3187,10 @@ func (mr *MockDownstreamStoreMockRecorder) GetDownstreamVersionSource(appID, seq } // GetDownstreamVersionStatus mocks base method. -func (m *MockDownstreamStore) GetDownstreamVersionStatus(appID string, sequence int64) (types11.DownstreamVersionStatus, error) { +func (m *MockDownstreamStore) GetDownstreamVersionStatus(appID string, sequence int64) (types12.DownstreamVersionStatus, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetDownstreamVersionStatus", appID, sequence) - ret0, _ := ret[0].(types11.DownstreamVersionStatus) + ret0, _ := ret[0].(types12.DownstreamVersionStatus) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -3233,10 +3279,10 @@ func (mr *MockDownstreamStoreMockRecorder) GetPreviouslyDeployedSequence(appID, } // GetStatusForVersion mocks base method. -func (m *MockDownstreamStore) GetStatusForVersion(appID, clusterID string, sequence int64) (types11.DownstreamVersionStatus, error) { +func (m *MockDownstreamStore) GetStatusForVersion(appID, clusterID string, sequence int64) (types12.DownstreamVersionStatus, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetStatusForVersion", appID, clusterID, sequence) - ret0, _ := ret[0].(types11.DownstreamVersionStatus) + ret0, _ := ret[0].(types12.DownstreamVersionStatus) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -3293,7 +3339,7 @@ func (mr *MockDownstreamStoreMockRecorder) MarkAsCurrentDownstreamVersion(appID, } // SetDownstreamVersionStatus mocks base method. -func (m *MockDownstreamStore) SetDownstreamVersionStatus(appID string, sequence int64, status types11.DownstreamVersionStatus, statusInfo string) error { +func (m *MockDownstreamStore) SetDownstreamVersionStatus(appID string, sequence int64, status types12.DownstreamVersionStatus, statusInfo string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SetDownstreamVersionStatus", appID, sequence, status, statusInfo) ret0, _ := ret[0].(error) @@ -3481,7 +3527,7 @@ func (m *MockVersionStore) EXPECT() *MockVersionStoreMockRecorder { } // CreateAppVersion mocks base method. -func (m *MockVersionStore) CreateAppVersion(appID string, baseSequence *int64, filesInDir, source string, isInstall, isAutomated bool, configFile string, skipPreflights bool, renderer types9.Renderer) (int64, error) { +func (m *MockVersionStore) CreateAppVersion(appID string, baseSequence *int64, filesInDir, source string, isInstall, isAutomated bool, configFile string, skipPreflights bool, renderer types10.Renderer) (int64, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateAppVersion", appID, baseSequence, filesInDir, source, isInstall, isAutomated, configFile, skipPreflights, renderer) ret0, _ := ret[0].(int64) @@ -3496,7 +3542,7 @@ func (mr *MockVersionStoreMockRecorder) CreateAppVersion(appID, baseSequence, fi } // CreatePendingDownloadAppVersion mocks base method. -func (m *MockVersionStore) CreatePendingDownloadAppVersion(appID string, update types13.Update, kotsApplication *v1beta10.Application, license *v1beta10.License) (int64, error) { +func (m *MockVersionStore) CreatePendingDownloadAppVersion(appID string, update types14.Update, kotsApplication *v1beta10.Application, license *v1beta10.License) (int64, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreatePendingDownloadAppVersion", appID, update, kotsApplication, license) ret0, _ := ret[0].(int64) @@ -3691,7 +3737,7 @@ func (mr *MockVersionStoreMockRecorder) IsRollbackSupportedForVersion(appID, seq } // IsSnapshotsSupportedForVersion mocks base method. -func (m *MockVersionStore) IsSnapshotsSupportedForVersion(a *types3.App, sequence int64, renderer types9.Renderer) (bool, error) { +func (m *MockVersionStore) IsSnapshotsSupportedForVersion(a *types3.App, sequence int64, renderer types10.Renderer) (bool, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "IsSnapshotsSupportedForVersion", a, sequence, renderer) ret0, _ := ret[0].(bool) @@ -3706,7 +3752,7 @@ func (mr *MockVersionStoreMockRecorder) IsSnapshotsSupportedForVersion(a, sequen } // UpdateAppVersion mocks base method. -func (m *MockVersionStore) UpdateAppVersion(appID string, sequence int64, baseSequence *int64, filesInDir, source string, skipPreflights bool, renderer types9.Renderer) error { +func (m *MockVersionStore) UpdateAppVersion(appID string, sequence int64, baseSequence *int64, filesInDir, source string, skipPreflights bool, renderer types10.Renderer) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateAppVersion", appID, sequence, baseSequence, filesInDir, source, skipPreflights, renderer) ret0, _ := ret[0].(error) @@ -3802,7 +3848,7 @@ func (mr *MockLicenseStoreMockRecorder) GetLicenseForAppVersion(appID, sequence } // UpdateAppLicense mocks base method. -func (m *MockLicenseStore) UpdateAppLicense(appID string, sequence int64, archiveDir string, newLicense *v1beta10.License, originalLicenseData string, channelChanged, failOnVersionCreate bool, renderer types9.Renderer, reportingInfo *types1.ReportingInfo) (int64, error) { +func (m *MockLicenseStore) UpdateAppLicense(appID string, sequence int64, archiveDir string, newLicense *v1beta10.License, originalLicenseData string, channelChanged, failOnVersionCreate bool, renderer types10.Renderer, reportingInfo *types1.ReportingInfo) (int64, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateAppLicense", appID, sequence, archiveDir, newLicense, originalLicenseData, channelChanged, failOnVersionCreate, renderer, reportingInfo) ret0, _ := ret[0].(int64) @@ -4299,3 +4345,71 @@ func (mr *MockEmbeddedClusterStoreMockRecorder) SetEmbeddedClusterInstallCommand mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetEmbeddedClusterInstallCommandRoles", reflect.TypeOf((*MockEmbeddedClusterStore)(nil).SetEmbeddedClusterInstallCommandRoles), roles) } + +// MockPlanStore is a mock of PlanStore interface. +type MockPlanStore struct { + ctrl *gomock.Controller + recorder *MockPlanStoreMockRecorder +} + +// MockPlanStoreMockRecorder is the mock recorder for MockPlanStore. +type MockPlanStoreMockRecorder struct { + mock *MockPlanStore +} + +// NewMockPlanStore creates a new mock instance. +func NewMockPlanStore(ctrl *gomock.Controller) *MockPlanStore { + mock := &MockPlanStore{ctrl: ctrl} + mock.recorder = &MockPlanStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPlanStore) EXPECT() *MockPlanStoreMockRecorder { + return m.recorder +} + +// GetCurrentPlan mocks base method. +func (m *MockPlanStore) GetCurrentPlan(appID string) (*types7.Plan, *time.Time, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCurrentPlan", appID) + ret0, _ := ret[0].(*types7.Plan) + ret1, _ := ret[1].(*time.Time) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetCurrentPlan indicates an expected call of GetCurrentPlan. +func (mr *MockPlanStoreMockRecorder) GetCurrentPlan(appID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentPlan", reflect.TypeOf((*MockPlanStore)(nil).GetCurrentPlan), appID) +} + +// GetPlan mocks base method. +func (m *MockPlanStore) GetPlan(appID, versionLabel string) (*types7.Plan, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPlan", appID, versionLabel) + ret0, _ := ret[0].(*types7.Plan) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPlan indicates an expected call of GetPlan. +func (mr *MockPlanStoreMockRecorder) GetPlan(appID, versionLabel interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPlan", reflect.TypeOf((*MockPlanStore)(nil).GetPlan), appID, versionLabel) +} + +// UpsertPlan mocks base method. +func (m *MockPlanStore) UpsertPlan(plan *types7.Plan) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpsertPlan", plan) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpsertPlan indicates an expected call of UpsertPlan. +func (mr *MockPlanStoreMockRecorder) UpsertPlan(plan interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertPlan", reflect.TypeOf((*MockPlanStore)(nil).UpsertPlan), plan) +} diff --git a/pkg/store/store_interface.go b/pkg/store/store_interface.go index 8320a13f66..a0c35fdbb0 100644 --- a/pkg/store/store_interface.go +++ b/pkg/store/store_interface.go @@ -13,6 +13,7 @@ import ( appstatetypes "github.com/replicatedhq/kots/pkg/appstate/types" snapshottypes "github.com/replicatedhq/kots/pkg/kotsadmsnapshot/types" installationtypes "github.com/replicatedhq/kots/pkg/online/types" + plantypes "github.com/replicatedhq/kots/pkg/plan/types" preflighttypes "github.com/replicatedhq/kots/pkg/preflight/types" registrytypes "github.com/replicatedhq/kots/pkg/registry/types" rendertypes "github.com/replicatedhq/kots/pkg/render/types" @@ -46,6 +47,7 @@ type Store interface { EmbeddedStore BrandingStore EmbeddedClusterStore + PlanStore Init() error // this may need options WaitForReady(ctx context.Context) error @@ -245,3 +247,9 @@ type EmbeddedClusterStore interface { SetEmbeddedClusterInstallCommandRoles(roles []string) (string, error) GetEmbeddedClusterInstallCommandRoles(token string) ([]string, error) } + +type PlanStore interface { + GetPlan(appID, versionLabel string) (*plantypes.Plan, error) + UpsertPlan(plan *plantypes.Plan) error + GetCurrentPlan(appID string) (*plantypes.Plan, *time.Time, error) +} diff --git a/pkg/upgradeservice/bootstrap.go b/pkg/upgradeservice/bootstrap.go index f238639cd2..d8e4c29385 100644 --- a/pkg/upgradeservice/bootstrap.go +++ b/pkg/upgradeservice/bootstrap.go @@ -13,8 +13,10 @@ import ( identity "github.com/replicatedhq/kots/pkg/kotsadmidentity" "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/logger" + plantypes "github.com/replicatedhq/kots/pkg/plan/types" "github.com/replicatedhq/kots/pkg/pull" registrytypes "github.com/replicatedhq/kots/pkg/registry/types" + "github.com/replicatedhq/kots/pkg/upgradeservice/plan" upgradepreflight "github.com/replicatedhq/kots/pkg/upgradeservice/preflight" "github.com/replicatedhq/kots/pkg/upgradeservice/task" "github.com/replicatedhq/kots/pkg/upgradeservice/types" @@ -105,8 +107,14 @@ func pullArchive(params types.UpgradeServiceParams, pullOptions pull.PullOptions go func() { scanner := bufio.NewScanner(pipeReader) for scanner.Scan() { - if err := task.SetStatusStarting(params.AppSlug, scanner.Text()); err != nil { - logger.Error(err) + if util.IsEC2Install() { + if err := plan.UpdateStepStatus(params, plantypes.StepStatusStarting, scanner.Text(), ""); err != nil { + logger.Error(err) + } + } else { + if err := task.SetStatusStarting(params.AppSlug, scanner.Text()); err != nil { + logger.Error(err) + } } } pipeReader.CloseWithError(scanner.Err()) diff --git a/pkg/upgradeservice/deploy/deploy_ec2.go b/pkg/upgradeservice/deploy/deploy_ec2.go new file mode 100644 index 0000000000..b47b0ac47d --- /dev/null +++ b/pkg/upgradeservice/deploy/deploy_ec2.go @@ -0,0 +1,99 @@ +package deploy + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/pkg/errors" + "github.com/replicatedhq/kots/pkg/apparchive" + kotsadmconfig "github.com/replicatedhq/kots/pkg/kotsadmconfig" + "github.com/replicatedhq/kots/pkg/kotsutil" + plantypes "github.com/replicatedhq/kots/pkg/plan/types" + registrytypes "github.com/replicatedhq/kots/pkg/registry/types" + "github.com/replicatedhq/kots/pkg/upgradeservice/plan" + upgradepreflight "github.com/replicatedhq/kots/pkg/upgradeservice/preflight" + "github.com/replicatedhq/kots/pkg/upgradeservice/types" +) + +type CanDeployEC2Options struct { + Params types.UpgradeServiceParams + KotsKinds *kotsutil.KotsKinds + RegistrySettings registrytypes.RegistrySettings +} + +func CanDeployEC2(opts CanDeployEC2Options) (bool, string, error) { + needsConfig, err := kotsadmconfig.NeedsConfiguration( + opts.Params.AppSlug, + opts.Params.NextSequence, + opts.Params.AppIsAirgap, + opts.KotsKinds, + opts.RegistrySettings, + ) + if err != nil { + return false, "", errors.Wrap(err, "check if version needs configuration") + } + if needsConfig { + return false, "cannot deploy because version needs configuration", nil + } + + pd, err := upgradepreflight.GetPreflightData() + if err != nil { + return false, "", errors.Wrap(err, "get preflight data") + } + if pd.Result != nil && pd.Result.HasFailingStrictPreflights { + return false, "cannot deploy because a strict preflight check has failed", nil + } + + return true, "", nil +} + +type DeployEC2Options struct { + Ctx context.Context + IsSkipPreflights bool + ContinueWithFailedPreflights bool + Params types.UpgradeServiceParams + KotsKinds *kotsutil.KotsKinds + RegistrySettings registrytypes.RegistrySettings +} + +func DeployEC2(opts DeployEC2Options) error { + // put the app version archive in the object store so the operator + // of the new kots version can retrieve it to deploy the app + tgzArchiveKey := fmt.Sprintf( + "deployments/%s/%s-%s.tar.gz", + opts.Params.AppSlug, + opts.Params.UpdateChannelID, + opts.Params.UpdateCursor, + ) + if err := apparchive.CreateAppVersionArchive(opts.Params.AppArchive, tgzArchiveKey); err != nil { + return errors.Wrap(err, "create app version archive") + } + + preflightData, err := upgradepreflight.GetPreflightData() + if err != nil { + return errors.Wrap(err, "get preflight data") + } + + preflightResult := "" + if preflightData.Result != nil { + preflightResult = preflightData.Result.Result + } + + stepOutput, err := json.Marshal(map[string]string{ + "app-version-archive": tgzArchiveKey, + "source": opts.Params.Source, + "skip-preflights": fmt.Sprintf("%t", opts.IsSkipPreflights), + "continue-with-failed-preflights": fmt.Sprintf("%t", opts.ContinueWithFailedPreflights), + "preflight-result": preflightResult, + }) + if err != nil { + return errors.Wrap(err, "marshal data") + } + + if err := plan.UpdateStepStatus(opts.Params, plantypes.StepStatusComplete, "", string(stepOutput)); err != nil { + return errors.Wrap(err, "update step status") + } + + return nil +} diff --git a/pkg/upgradeservice/handlers/deploy_ec2.go b/pkg/upgradeservice/handlers/deploy_ec2.go new file mode 100644 index 0000000000..aa973e551b --- /dev/null +++ b/pkg/upgradeservice/handlers/deploy_ec2.go @@ -0,0 +1,89 @@ +package handlers + +import ( + "encoding/json" + "net/http" + + "github.com/pkg/errors" + "github.com/replicatedhq/kots/pkg/kotsutil" + "github.com/replicatedhq/kots/pkg/logger" + registrytypes "github.com/replicatedhq/kots/pkg/registry/types" + "github.com/replicatedhq/kots/pkg/upgradeservice/deploy" +) + +type DeployEC2Request struct { + IsSkipPreflights bool `json:"isSkipPreflights"` + ContinueWithFailedPreflights bool `json:"continueWithFailedPreflights"` +} + +type DeployEC2Response struct { + Success bool `json:"success"` + Error string `json:"error,omitempty"` +} + +func (h *Handler) DeployEC2(w http.ResponseWriter, r *http.Request) { + response := DeployEC2Response{ + Success: false, + } + + params := GetContextParams(r) + + request := DeployEC2Request{} + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + response.Error = "failed to decode request body" + logger.Error(errors.Wrap(err, response.Error)) + JSON(w, http.StatusBadRequest, response) + return + } + + kotsKinds, err := kotsutil.LoadKotsKinds(params.AppArchive) + if err != nil { + response.Error = "failed to load kots kinds from path" + logger.Error(errors.Wrap(err, response.Error)) + JSON(w, http.StatusInternalServerError, response) + return + } + + registrySettings := registrytypes.RegistrySettings{ + Hostname: params.RegistryEndpoint, + Username: params.RegistryUsername, + Password: params.RegistryPassword, + Namespace: params.RegistryNamespace, + IsReadOnly: params.RegistryIsReadOnly, + } + + canDeploy, reason, err := deploy.CanDeployEC2(deploy.CanDeployEC2Options{ + Params: params, + KotsKinds: kotsKinds, + RegistrySettings: registrySettings, + }) + if err != nil { + response.Error = "failed to check if app can be deployed" + logger.Error(errors.Wrap(err, response.Error)) + JSON(w, http.StatusInternalServerError, response) + return + } + if !canDeploy { + response.Error = reason + logger.Error(errors.New(response.Error)) + JSON(w, http.StatusBadRequest, response) + return + } + + if err := deploy.DeployEC2(deploy.DeployEC2Options{ + Ctx: r.Context(), + IsSkipPreflights: request.IsSkipPreflights, + ContinueWithFailedPreflights: request.ContinueWithFailedPreflights, + Params: params, + KotsKinds: kotsKinds, + RegistrySettings: registrySettings, + }); err != nil { + response.Error = "failed to deploy app" + logger.Error(errors.Wrap(err, response.Error)) + JSON(w, http.StatusInternalServerError, response) + return + } + + response.Success = true + JSON(w, http.StatusOK, response) +} diff --git a/pkg/upgradeservice/handlers/handlers.go b/pkg/upgradeservice/handlers/handlers.go index 7c2dc3782b..305e99b996 100644 --- a/pkg/upgradeservice/handlers/handlers.go +++ b/pkg/upgradeservice/handlers/handlers.go @@ -40,6 +40,7 @@ func RegisterAPIRoutes(r *mux.Router, handler UpgradeServiceHandler) { subRouter.Path("/preflight/result").Methods("GET").HandlerFunc(handler.GetPreflightResult) subRouter.Path("/deploy").Methods("POST").HandlerFunc(handler.Deploy) + subRouter.Path("/ec2-deploy").Methods("POST").HandlerFunc(handler.DeployEC2) } func JSON(w http.ResponseWriter, code int, payload interface{}) { diff --git a/pkg/upgradeservice/handlers/info.go b/pkg/upgradeservice/handlers/info.go index ccdb94c310..d3c0efe407 100644 --- a/pkg/upgradeservice/handlers/info.go +++ b/pkg/upgradeservice/handlers/info.go @@ -6,6 +6,7 @@ import ( "github.com/pkg/errors" "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/logger" + "github.com/replicatedhq/kots/pkg/util" ) type InfoResponse struct { @@ -13,6 +14,7 @@ type InfoResponse struct { Error string `json:"error,omitempty"` HasPreflight bool `json:"hasPreflight"` IsConfigurable bool `json:"isConfigurable"` + IsEC2Install bool `json:"isEC2Install"` } func (h *Handler) Info(w http.ResponseWriter, r *http.Request) { @@ -33,6 +35,7 @@ func (h *Handler) Info(w http.ResponseWriter, r *http.Request) { response.Success = true response.HasPreflight = kotsKinds.HasPreflights() response.IsConfigurable = kotsKinds.IsConfigurable() + response.IsEC2Install = util.IsEC2Install() JSON(w, http.StatusOK, response) } diff --git a/pkg/upgradeservice/handlers/interface.go b/pkg/upgradeservice/handlers/interface.go index ccb467a5cb..99500cd7a0 100644 --- a/pkg/upgradeservice/handlers/interface.go +++ b/pkg/upgradeservice/handlers/interface.go @@ -15,4 +15,5 @@ type UpgradeServiceHandler interface { GetPreflightResult(w http.ResponseWriter, r *http.Request) Deploy(w http.ResponseWriter, r *http.Request) + DeployEC2(w http.ResponseWriter, r *http.Request) } diff --git a/pkg/upgradeservice/plan/plan.go b/pkg/upgradeservice/plan/plan.go new file mode 100644 index 0000000000..d05d4cde75 --- /dev/null +++ b/pkg/upgradeservice/plan/plan.go @@ -0,0 +1,44 @@ +package plan + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + + "github.com/pkg/errors" + plantypes "github.com/replicatedhq/kots/pkg/plan/types" + "github.com/replicatedhq/kots/pkg/upgradeservice/types" +) + +func UpdateStepStatus(params types.UpgradeServiceParams, status plantypes.PlanStepStatus, description string, output string) error { + body, err := json.Marshal(map[string]string{ + "versionLabel": params.UpdateVersionLabel, + "status": string(status), + "statusDescription": description, + "output": output, + }) + if err != nil { + return errors.Wrap(err, "marshal request body") + } + + url := fmt.Sprintf("http://localhost:3000/api/v1/app/%s/plan/%s", params.AppSlug, params.PlanStepID) + req, err := http.NewRequest("PUT", url, bytes.NewBuffer(body)) + if err != nil { + return errors.Wrap(err, "create request") + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return errors.Wrap(err, "send request") + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return errors.Errorf("send request, status code: %d", resp.StatusCode) + } + + return nil +} diff --git a/pkg/upgradeservice/types/types.go b/pkg/upgradeservice/types/types.go index d5c2bdcda3..43dae0aea0 100644 --- a/pkg/upgradeservice/types/types.go +++ b/pkg/upgradeservice/types/types.go @@ -5,34 +5,35 @@ import ( ) type UpgradeServiceParams struct { - Port string `yaml:"port"` - - AppID string `yaml:"appId"` - AppSlug string `yaml:"appSlug"` - AppName string `yaml:"appName"` - AppIsAirgap bool `yaml:"appIsAirgap"` - AppIsGitOps bool `yaml:"appIsGitOps"` - AppLicense string `yaml:"appLicense"` - AppArchive string `yaml:"appArchive"` - - Source string `yaml:"source"` - BaseSequence int64 `yaml:"baseSequence"` - NextSequence int64 `yaml:"nextSequence"` - - UpdateVersionLabel string `yaml:"updateVersionLabel"` - UpdateCursor string `yaml:"updateCursor"` - UpdateChannelID string `yaml:"updateChannelID"` - UpdateECVersion string `yaml:"updateECVersion"` - UpdateKOTSBin string `yaml:"updateKotsBin"` - UpdateAirgapBundle string `yaml:"updateAirgapBundle"` - - CurrentECVersion string `yaml:"currentECVersion"` - - RegistryEndpoint string `yaml:"registryEndpoint"` - RegistryUsername string `yaml:"registryUsername"` - RegistryPassword string `yaml:"registryPassword"` - RegistryNamespace string `yaml:"registryNamespace"` - RegistryIsReadOnly bool `yaml:"registryIsReadOnly"` - - ReportingInfo *reportingtypes.ReportingInfo `yaml:"reportingInfo"` + Port string `yaml:"port" json:"port"` + PlanStepID string `yaml:"planStepID" json:"planStepID"` + + AppID string `yaml:"appId" json:"appId"` + AppSlug string `yaml:"appSlug" json:"appSlug"` + AppName string `yaml:"appName" json:"appName"` + AppIsAirgap bool `yaml:"appIsAirgap" json:"appIsAirgap"` + AppIsGitOps bool `yaml:"appIsGitOps" json:"appIsGitOps"` + AppLicense string `yaml:"appLicense" json:"appLicense"` + AppArchive string `yaml:"appArchive" json:"appArchive"` + + Source string `yaml:"source" json:"source"` + BaseSequence int64 `yaml:"baseSequence" json:"baseSequence"` + NextSequence int64 `yaml:"nextSequence" json:"nextSequence"` + + UpdateVersionLabel string `yaml:"updateVersionLabel" json:"updateVersionLabel"` + UpdateCursor string `yaml:"updateCursor" json:"updateCursor"` + UpdateChannelID string `yaml:"updateChannelID" json:"updateChannelID"` + UpdateECVersion string `yaml:"updateECVersion" json:"updateECVersion"` + UpdateKOTSBin string `yaml:"updateKotsBin" json:"updateKotsBin"` + UpdateAirgapBundle string `yaml:"updateAirgapBundle" json:"updateAirgapBundle"` + + CurrentECVersion string `yaml:"currentECVersion" json:"currentECVersion"` + + RegistryEndpoint string `yaml:"registryEndpoint" json:"registryEndpoint"` + RegistryUsername string `yaml:"registryUsername" json:"registryUsername"` + RegistryPassword string `yaml:"registryPassword" json:"registryPassword"` + RegistryNamespace string `yaml:"registryNamespace" json:"registryNamespace"` + RegistryIsReadOnly bool `yaml:"registryIsReadOnly" json:"registryIsReadOnly"` + + ReportingInfo *reportingtypes.ReportingInfo `yaml:"reportingInfo" json:"reportingInfo"` } diff --git a/pkg/upstream/replicated.go b/pkg/upstream/replicated.go index a566342bb2..a5ab33c1e3 100644 --- a/pkg/upstream/replicated.go +++ b/pkg/upstream/replicated.go @@ -355,7 +355,7 @@ func readReplicatedAppFromLocalPath(localPath string, localCursor replicatedapp. } func downloadReplicatedApp(replicatedUpstream *replicatedapp.ReplicatedUpstream, license *kotsv1beta1.License, cursor replicatedapp.ReplicatedCursor, reportingInfo *reportingtypes.ReportingInfo, selectedAppChannel string) (*Release, error) { - getReq, err := replicatedUpstream.GetRequest("GET", license, cursor, selectedAppChannel) + getReq, err := replicatedUpstream.GetRequest("GET", license, cursor.Cursor, selectedAppChannel) if err != nil { return nil, errors.Wrap(err, "failed to create http request") } diff --git a/pkg/util/util.go b/pkg/util/util.go index 9924d4186a..0f4ae90718 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -172,6 +172,10 @@ func EmbeddedClusterVersion() string { return os.Getenv("EMBEDDED_CLUSTER_VERSION") } +func IsEC2Install() bool { + return os.Getenv("IS_EC2_INSTALL") == "true" +} + func IsUpgradeService() bool { return os.Getenv("IS_UPGRADE_SERVICE") == "true" } diff --git a/pkg/websocket/upgrade.go b/pkg/websocket/upgrade.go new file mode 100644 index 0000000000..b801ccf174 --- /dev/null +++ b/pkg/websocket/upgrade.go @@ -0,0 +1,128 @@ +package websocket + +import ( + "encoding/json" + + "github.com/gorilla/websocket" + k0sv1beta1 "github.com/k0sproject/k0s/pkg/apis/k0s/v1beta1" + "github.com/pkg/errors" + ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1" + "github.com/replicatedhq/kots/pkg/logger" + "github.com/replicatedhq/kots/pkg/websocket/types" +) + +// UpgradeCluster sends an upgrade command to the first available websocket from the active ones +func UpgradeCluster(installation *ecv1beta1.Installation, appSlug, versionLabel, stepID string) error { + marshalledInst, err := json.Marshal(installation) + if err != nil { + return errors.Wrap(err, "marshal installation") + } + + data, err := json.Marshal(map[string]string{ + "installation": string(marshalledInst), + "appSlug": appSlug, + "versionLabel": versionLabel, + "stepID": stepID, + }) + if err != nil { + return errors.Wrap(err, "marshal installation") + } + + message, err := json.Marshal(map[string]interface{}{ + "command": "upgrade-cluster", + "data": string(data), + }) + if err != nil { + return errors.Wrap(err, "marshal command message") + } + + wscli, nodeName, err := firstActiveWSClient() + if err != nil { + return errors.Wrap(err, "get first active websocket client") + } + + logger.Infof("Sending cluster upgrade command to websocket of node %s with message: %s", nodeName, string(message)) + + if err := wscli.Conn.WriteMessage(websocket.TextMessage, message); err != nil { + return errors.Wrap(err, "send upgrade command to websocket") + } + + return nil +} + +func AddExtension(repos []k0sv1beta1.Repository, chart ecv1beta1.Chart, appSlug, versionLabel, stepID string) error { + return sendExtensionCommand("add-extension", repos, chart, appSlug, versionLabel, stepID) +} + +func UpgradeExtension(repos []k0sv1beta1.Repository, chart ecv1beta1.Chart, appSlug, versionLabel, stepID string) error { + return sendExtensionCommand("upgrade-extension", repos, chart, appSlug, versionLabel, stepID) +} + +func RemoveExtension(repos []k0sv1beta1.Repository, chart ecv1beta1.Chart, appSlug, versionLabel, stepID string) error { + return sendExtensionCommand("remove-extension", repos, chart, appSlug, versionLabel, stepID) +} + +func sendExtensionCommand(command string, repos []k0sv1beta1.Repository, chart ecv1beta1.Chart, appSlug, versionLabel, stepID string) error { + marshalledRepos, err := json.Marshal(repos) + if err != nil { + return errors.Wrap(err, "marshal repos") + } + + marshalledChart, err := json.Marshal(chart) + if err != nil { + return errors.Wrap(err, "marshal chart") + } + + data, err := json.Marshal(map[string]string{ + "repos": string(marshalledRepos), + "chart": string(marshalledChart), + "appSlug": appSlug, + "versionLabel": versionLabel, + "stepID": stepID, + }) + if err != nil { + return errors.Wrap(err, "marshal data") + } + + message, err := json.Marshal(map[string]interface{}{ + "command": command, + "data": string(data), + }) + if err != nil { + return errors.Wrap(err, "marshal command message") + } + + wscli, nodeName, err := firstActiveWSClient() + if err != nil { + return errors.Wrap(err, "get first active websocket client") + } + + logger.Infof("Sending extension %s command to websocket of node %s with message: %s", command, nodeName, string(message)) + + if err := wscli.Conn.WriteMessage(websocket.TextMessage, message); err != nil { + return errors.Wrap(err, "send upgrade command to websocket") + } + + return nil +} + +func firstActiveWSClient() (types.WSClient, string, error) { + wsMutex.Lock() + defer wsMutex.Unlock() + + if len(wsClients) == 0 { + return types.WSClient{}, "", errors.New("no active websocket connections available") + } + + var wscli types.WSClient + var nodeName string + + // get the first client in the map + for name, client := range wsClients { + nodeName = name + wscli = client + break + } + + return wscli, nodeName, nil +} diff --git a/web/src/Root.tsx b/web/src/Root.tsx index d2d23d6484..92d05089d1 100644 --- a/web/src/Root.tsx +++ b/web/src/Root.tsx @@ -278,16 +278,17 @@ const Root = () => { const fetchUpgradeStatus = async (appSlug) => { try { - const res = await fetch( - `${process.env.API_ENDPOINT}/app/${appSlug}/task/upgrade-service`, - { - headers: { - "Content-Type": "application/json", - }, - credentials: "include", - method: "GET", - } - ); + let url = `${process.env.API_ENDPOINT}/app/${appSlug}/task/upgrade-service`; + if (state.adminConsoleMetadata?.isEC2Install) { + url = `${process.env.API_ENDPOINT}/app/${appSlug}/ec2-deploy/status`; + } + const res = await fetch(url, { + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + method: "GET", + }); if (!res.ok) { if (res.status === 401) { Utilities.logoutUser(); @@ -301,11 +302,21 @@ const Root = () => { } const response = await res.json(); const status = response.status; - if ( - status === "upgrading-cluster" || - status === "upgrading-app" || - status === "upgrade-failed" - ) { + + let showModal = false; + if (state.adminConsoleMetadata?.isEC2Install) { + // TODO (@salah): don't show modal if it's only an app upgrade + showModal = + status !== "" && + status !== "complete" && + response.step !== "app-upgrade-service"; + } else { + showModal = + status === "upgrading-cluster" || + status === "upgrading-app" || + status === "upgrade-failed"; + } + if (showModal) { setState({ showUpgradeStatusModal: true, upgradeStatus: status, @@ -1018,6 +1029,7 @@ const Root = () => { refetchUpgradeStatus={fetchUpgradeStatus} snapshotInProgressApps={state.snapshotInProgressApps} ping={ping} + adminConsoleMetadata={state.adminConsoleMetadata} isEmbeddedCluster={Boolean( state.adminConsoleMetadata?.isEmbeddedCluster )} @@ -1203,7 +1215,10 @@ const Root = () => { isOpen={state.showUpgradeStatusModal} onRequestClose={() => { // cannot close the modal while upgrading - if (state.upgradeStatus === "upgrade-failed") { + const failedStatus = state.adminConsoleMetadata?.isEC2Install + ? "failed" + : "upgrade-failed"; + if (state.upgradeStatus === failedStatus) { setState({ showUpgradeStatusModal: false }); } }} @@ -1222,6 +1237,7 @@ const Root = () => { setTerminatedState={(status: boolean) => setState({ connectionTerminated: status }) } + isEC2Install={state.adminConsoleMetadata?.isEC2Install} /> ) : ( diff --git a/web/src/components/apps/AppVersionHistory.tsx b/web/src/components/apps/AppVersionHistory.tsx index 084c614292..93f8fffbc7 100644 --- a/web/src/components/apps/AppVersionHistory.tsx +++ b/web/src/components/apps/AppVersionHistory.tsx @@ -63,6 +63,7 @@ type Props = { isAirgap: boolean; isKurl: boolean; isEmbeddedCluster: boolean; + isEC2Install: boolean; }; app: App; displayErrorModal: boolean; @@ -1024,23 +1025,32 @@ class AppVersionHistory extends Component { }; onCheckForUpgradeServiceStatus = async () => { - const { app } = this.props.outletContext; + const { app, adminConsoleMetadata } = this.props.outletContext; this.setState({ isStartingUpgradeService: true }); return new Promise((resolve, reject) => { - fetch( - `${process.env.API_ENDPOINT}/app/${app?.slug}/task/upgrade-service`, - { - headers: { - "Content-Type": "application/json", - }, - credentials: "include", - method: "GET", - } - ) + let url = `${process.env.API_ENDPOINT}/app/${app?.slug}/task/upgrade-service`; + if (adminConsoleMetadata?.isEC2Install) { + url = `${process.env.API_ENDPOINT}/app/${app?.slug}/ec2-deploy/status`; + } + fetch(url, { + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + method: "GET", + }) .then(async (res) => { const response = await res.json(); - if (response.status !== "starting") { + + let stopPolling = response.status !== "starting"; + if (adminConsoleMetadata?.isEC2Install) { + stopPolling = + response.step !== "app-upgrade-service" || + (response.status !== "pending" && response.status !== "starting"); + } + + if (stopPolling) { this.state.upgradeServiceChecker.stop(); this.setState({ isStartingUpgradeService: false, @@ -1522,8 +1532,14 @@ class AppVersionHistory extends Component { error: "", }, }); + const appSlug = this.props.params.slug; - fetch(`${process.env.API_ENDPOINT}/app/${appSlug}/start-upgrade-service`, { + let url = `${process.env.API_ENDPOINT}/app/${appSlug}/start-upgrade-service`; + if (this.props.outletContext.adminConsoleMetadata?.isEC2Install) { + url = `${process.env.API_ENDPOINT}/app/${appSlug}/ec2-deploy`; + } + + fetch(url, { headers: { "Content-Type": "application/json", Accept: "application/json", diff --git a/web/src/components/modals/UpgradeStatusModal.tsx b/web/src/components/modals/UpgradeStatusModal.tsx index 8a5235c528..033f5e7bdf 100644 --- a/web/src/components/modals/UpgradeStatusModal.tsx +++ b/web/src/components/modals/UpgradeStatusModal.tsx @@ -11,9 +11,12 @@ interface Props { closeModal: () => void; connectionTerminated: boolean; setTerminatedState: (terminated: boolean) => void; + isEC2Install: boolean; } const UpgradeStatusModal = (props: Props) => { + const failedStatus = props.isEC2Install ? "failed" : "upgrade-failed"; + const ping = async () => { await fetch(`${process.env.API_ENDPOINT}/ping`, { headers: { @@ -40,14 +43,14 @@ const UpgradeStatusModal = (props: Props) => { const interval = setInterval(() => { if (props.connectionTerminated) { ping(); - } else if (props.status !== "upgrade-failed") { + } else if (props.status !== failedStatus) { props.refetchStatus(props.appSlug); } }, 10000); return () => clearInterval(interval); }, [props.connectionTerminated, props.status]); - if (props.status === "upgrade-failed") { + if (props.status === failedStatus) { return (
@@ -78,11 +81,13 @@ const UpgradeStatusModal = (props: Props) => { ); } - let status; + let message; if (props.status === "upgrading-cluster") { - status = Utilities.humanReadableClusterState(props.message); + message = Utilities.humanReadableClusterState(props.message); } else if (props.status === "upgrading-app") { - status = "Almost done"; + message = "Almost done"; + } else { + message = props.message; } return ( @@ -103,7 +108,7 @@ const UpgradeStatusModal = (props: Props) => { The page will automatically refresh when the update is complete.

- Status: {status} + Status: {message}

)}
diff --git a/web/src/components/upgrade_service/ConfirmAndDeploy.tsx b/web/src/components/upgrade_service/ConfirmAndDeploy.tsx index eecf0139c1..cf87f88a5a 100644 --- a/web/src/components/upgrade_service/ConfirmAndDeploy.tsx +++ b/web/src/components/upgrade_service/ConfirmAndDeploy.tsx @@ -63,10 +63,12 @@ const ConfirmAndDeploy = ({ setCurrentStep, hasPreflight, isConfigurable, + isEC2Install, }: { isConfigurable: boolean; hasPreflight: boolean; setCurrentStep: (step: number) => void; + isEC2Install: boolean; }) => { useEffect(() => { setCurrentStep(2); @@ -91,8 +93,8 @@ const ConfirmAndDeploy = ({ const { sequence = "0", slug } = useParams() as KotsParams; const { mutate: deployKotsDownstream, isLoading } = useDeployAppVersion({ slug, - sequence, closeModal, + isEC2Install: isEC2Install, }); const { data: preflightCheck, error: getPreflightResultsError } = diff --git a/web/src/components/upgrade_service/UpgradeService.tsx b/web/src/components/upgrade_service/UpgradeService.tsx index 316fce9a2e..5e70ac976c 100644 --- a/web/src/components/upgrade_service/UpgradeService.tsx +++ b/web/src/components/upgrade_service/UpgradeService.tsx @@ -96,6 +96,7 @@ const UpgradeServiceBody = () => { isConfigurable={upgradeInfo?.isConfigurable} hasPreflight={upgradeInfo?.hasPreflight} setCurrentStep={setCurrentStep} + isEC2Install={upgradeInfo?.isEC2Install} /> } /> diff --git a/web/src/components/upgrade_service/hooks/getUpgradeInfo.tsx b/web/src/components/upgrade_service/hooks/getUpgradeInfo.tsx index 2c6c87161f..534406bc3f 100644 --- a/web/src/components/upgrade_service/hooks/getUpgradeInfo.tsx +++ b/web/src/components/upgrade_service/hooks/getUpgradeInfo.tsx @@ -3,6 +3,7 @@ import { useQuery } from "@tanstack/react-query"; type UpgradeInfoResponse = { isConfigurable: boolean; hasPreflight: boolean; + isEC2Install: boolean; }; type UpgradeInfoParams = { diff --git a/web/src/components/upgrade_service/hooks/postDeployAppVersion.tsx b/web/src/components/upgrade_service/hooks/postDeployAppVersion.tsx index 231d5d1f0b..426cb12895 100644 --- a/web/src/components/upgrade_service/hooks/postDeployAppVersion.tsx +++ b/web/src/components/upgrade_service/hooks/postDeployAppVersion.tsx @@ -3,24 +3,25 @@ import { useMutation } from "@tanstack/react-query"; async function postDeployAppVersion({ slug, body, + isEC2Install, }: { - apiEndpoint?: string; body: string; slug: string; - sequence: string; + isEC2Install: boolean; }) { - const response = await fetch( - `${process.env.API_ENDPOINT}/upgrade-service/app/${slug}/deploy`, - { - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - credentials: "include", - method: "POST", - body, - } - ); + let url = `${process.env.API_ENDPOINT}/upgrade-service/app/${slug}/deploy`; + if (isEC2Install) { + url = `${process.env.API_ENDPOINT}/upgrade-service/app/${slug}/ec2-deploy`; + } + const response = await fetch(url, { + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + method: "POST", + body, + }); if (!response.ok) { throw new Error( @@ -44,12 +45,12 @@ function makeBody({ function useDeployAppVersion({ slug, - sequence, closeModal, + isEC2Install, }: { slug: string; - sequence: string; closeModal: () => void; + isEC2Install: boolean; }) { return useMutation({ mutationFn: ({ @@ -61,9 +62,8 @@ function useDeployAppVersion({ }) => postDeployAppVersion({ slug, - sequence, - body: makeBody({ continueWithFailedPreflights, isSkipPreflights }), + isEC2Install, }), onError: (err: Error) => { console.log(err); diff --git a/web/src/types/index.ts b/web/src/types/index.ts index 6feb0ab0f9..59715089a6 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -135,6 +135,7 @@ export type Metadata = { isAirgap: boolean; isKurl: boolean; isEmbeddedCluster: boolean; + isEC2Install: boolean; }; export type PreflightError = {