mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-08-03 12:36:09 -04:00
cli: helm install and upgrade unification (#2244)
This commit is contained in:
parent
9e79e2e0a1
commit
a03325466c
29 changed files with 1140 additions and 1054 deletions
|
@ -4,6 +4,7 @@ load("//bazel/go:go_test.bzl", "go_test")
|
|||
go_library(
|
||||
name = "kubecmd",
|
||||
srcs = [
|
||||
"backup.go",
|
||||
"kubecmd.go",
|
||||
"status.go",
|
||||
],
|
||||
|
@ -16,6 +17,7 @@ go_library(
|
|||
"//internal/compatibility",
|
||||
"//internal/config",
|
||||
"//internal/constants",
|
||||
"//internal/file",
|
||||
"//internal/imagefetcher",
|
||||
"//internal/kubernetes",
|
||||
"//internal/kubernetes/kubectl",
|
||||
|
@ -23,6 +25,7 @@ go_library(
|
|||
"//internal/versions/components",
|
||||
"//operators/constellation-node-operator/api/v1alpha1",
|
||||
"@io_k8s_api//core/v1:core",
|
||||
"@io_k8s_apiextensions_apiserver//pkg/apis/apiextensions/v1:apiextensions",
|
||||
"@io_k8s_apimachinery//pkg/api/errors",
|
||||
"@io_k8s_apimachinery//pkg/apis/meta/v1:meta",
|
||||
"@io_k8s_apimachinery//pkg/apis/meta/v1/unstructured",
|
||||
|
@ -37,7 +40,10 @@ go_library(
|
|||
|
||||
go_test(
|
||||
name = "kubecmd_test",
|
||||
srcs = ["kubecmd_test.go"],
|
||||
srcs = [
|
||||
"backup_test.go",
|
||||
"kubecmd_test.go",
|
||||
],
|
||||
embed = [":kubecmd"],
|
||||
deps = [
|
||||
"//internal/attestation/measurements",
|
||||
|
@ -46,18 +52,23 @@ go_test(
|
|||
"//internal/compatibility",
|
||||
"//internal/config",
|
||||
"//internal/constants",
|
||||
"//internal/file",
|
||||
"//internal/logger",
|
||||
"//internal/versions",
|
||||
"//internal/versions/components",
|
||||
"//operators/constellation-node-operator/api/v1alpha1",
|
||||
"@com_github_pkg_errors//:errors",
|
||||
"@com_github_spf13_afero//:afero",
|
||||
"@com_github_stretchr_testify//assert",
|
||||
"@com_github_stretchr_testify//mock",
|
||||
"@com_github_stretchr_testify//require",
|
||||
"@io_k8s_api//core/v1:core",
|
||||
"@io_k8s_apiextensions_apiserver//pkg/apis/apiextensions/v1:apiextensions",
|
||||
"@io_k8s_apimachinery//pkg/api/errors",
|
||||
"@io_k8s_apimachinery//pkg/apis/meta/v1:meta",
|
||||
"@io_k8s_apimachinery//pkg/apis/meta/v1/unstructured",
|
||||
"@io_k8s_apimachinery//pkg/runtime",
|
||||
"@io_k8s_apimachinery//pkg/runtime/schema",
|
||||
"@io_k8s_sigs_yaml//:yaml",
|
||||
],
|
||||
)
|
||||
|
|
115
cli/internal/kubecmd/backup.go
Normal file
115
cli/internal/kubecmd/backup.go
Normal file
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package kubecmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
type crdLister interface {
|
||||
ListCRDs(ctx context.Context) ([]apiextensionsv1.CustomResourceDefinition, error)
|
||||
ListCRs(ctx context.Context, gvr schema.GroupVersionResource) ([]unstructured.Unstructured, error)
|
||||
}
|
||||
|
||||
// BackupCRDs backs up all CRDs to the upgrade workspace.
|
||||
func (k *KubeCmd) BackupCRDs(ctx context.Context, upgradeDir string) ([]apiextensionsv1.CustomResourceDefinition, error) {
|
||||
k.log.Debugf("Starting CRD backup")
|
||||
crds, err := k.kubectl.ListCRDs(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting CRDs: %w", err)
|
||||
}
|
||||
|
||||
crdBackupFolder := k.crdBackupFolder(upgradeDir)
|
||||
if err := k.fileHandler.MkdirAll(crdBackupFolder); err != nil {
|
||||
return nil, fmt.Errorf("creating backup dir: %w", err)
|
||||
}
|
||||
for i := range crds {
|
||||
path := filepath.Join(crdBackupFolder, crds[i].Name+".yaml")
|
||||
|
||||
k.log.Debugf("Creating CRD backup: %s", path)
|
||||
|
||||
// We have to manually set kind/apiversion because of a long-standing limitation of the API:
|
||||
// https://github.com/kubernetes/kubernetes/issues/3030#issuecomment-67543738
|
||||
// The comment states that kind/version are encoded in the type.
|
||||
// The package holding the CRD type encodes the version.
|
||||
crds[i].Kind = "CustomResourceDefinition"
|
||||
crds[i].APIVersion = "apiextensions.k8s.io/v1"
|
||||
|
||||
yamlBytes, err := yaml.Marshal(crds[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := k.fileHandler.Write(path, yamlBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
k.log.Debugf("CRD backup complete")
|
||||
return crds, nil
|
||||
}
|
||||
|
||||
// BackupCRs backs up all CRs to the upgrade workspace.
|
||||
func (k *KubeCmd) BackupCRs(ctx context.Context, crds []apiextensionsv1.CustomResourceDefinition, upgradeDir string) error {
|
||||
k.log.Debugf("Starting CR backup")
|
||||
for _, crd := range crds {
|
||||
k.log.Debugf("Creating backup for resource type: %s", crd.Name)
|
||||
|
||||
// Iterate over all versions of the CRD
|
||||
// TODO: Consider iterating over crd.Status.StoredVersions instead
|
||||
// Currently, we have to ignore not-found errors, because a CRD might define
|
||||
// a version that is not installed in the cluster.
|
||||
// With the StoredVersions field, we could only iterate over the installed versions.
|
||||
for _, version := range crd.Spec.Versions {
|
||||
k.log.Debugf("Creating backup of CRs for %q at version %q", crd.Name, version.Name)
|
||||
|
||||
gvr := schema.GroupVersionResource{Group: crd.Spec.Group, Version: version.Name, Resource: crd.Spec.Names.Plural}
|
||||
crs, err := k.kubectl.ListCRs(ctx, gvr)
|
||||
if err != nil {
|
||||
if !k8serrors.IsNotFound(err) {
|
||||
return fmt.Errorf("retrieving CR %s: %w", crd.Name, err)
|
||||
}
|
||||
k.log.Debugf("No CRs found for %q at version %q, skipping...", crd.Name, version.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
backupFolder := k.backupFolder(upgradeDir)
|
||||
for _, cr := range crs {
|
||||
targetFolder := filepath.Join(backupFolder, gvr.Group, gvr.Version, cr.GetNamespace(), cr.GetKind())
|
||||
if err := k.fileHandler.MkdirAll(targetFolder); err != nil {
|
||||
return fmt.Errorf("creating resource dir: %w", err)
|
||||
}
|
||||
path := filepath.Join(targetFolder, cr.GetName()+".yaml")
|
||||
yamlBytes, err := yaml.Marshal(cr.Object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := k.fileHandler.Write(path, yamlBytes); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
k.log.Debugf("Backup for resource type %q complete", crd.Name)
|
||||
}
|
||||
k.log.Debugf("CR backup complete")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *KubeCmd) backupFolder(upgradeDir string) string {
|
||||
return filepath.Join(upgradeDir, "backups")
|
||||
}
|
||||
|
||||
func (k *KubeCmd) crdBackupFolder(upgradeDir string) string {
|
||||
return filepath.Join(k.backupFolder(upgradeDir), "crds")
|
||||
}
|
186
cli/internal/kubecmd/backup_test.go
Normal file
186
cli/internal/kubecmd/backup_test.go
Normal file
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package kubecmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/file"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
func TestBackupCRDs(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
upgradeID string
|
||||
crd string
|
||||
expectedFile string
|
||||
getCRDsError error
|
||||
wantError bool
|
||||
}{
|
||||
"success": {
|
||||
upgradeID: "1234",
|
||||
crd: "apiVersion: \nkind: \nmetadata:\n name: foobar\n creationTimestamp: null\nspec:\n group: \"\"\n names:\n kind: \"somename\"\n plural: \"somenames\"\n scope: \"\"\n versions: null\nstatus:\n acceptedNames:\n kind: \"\"\n plural: \"\"\n conditions: null\n storedVersions: null\n",
|
||||
expectedFile: "apiVersion: apiextensions.k8s.io/v1\nkind: CustomResourceDefinition\nmetadata:\n name: foobar\n creationTimestamp: null\nspec:\n group: \"\"\n names:\n kind: \"somename\"\n plural: \"somenames\"\n scope: \"\"\n versions: null\nstatus:\n acceptedNames:\n kind: \"\"\n plural: \"\"\n conditions: null\n storedVersions: null\n",
|
||||
},
|
||||
"api request fails": {
|
||||
upgradeID: "1234",
|
||||
getCRDsError: errors.New("api error"),
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
memFs := afero.NewMemMapFs()
|
||||
|
||||
crd := apiextensionsv1.CustomResourceDefinition{}
|
||||
err := yaml.Unmarshal([]byte(tc.crd), &crd)
|
||||
require.NoError(err)
|
||||
client := KubeCmd{
|
||||
kubectl: &stubKubectl{crds: []apiextensionsv1.CustomResourceDefinition{crd}, getCRDsError: tc.getCRDsError},
|
||||
fileHandler: file.NewHandler(memFs),
|
||||
log: stubLog{},
|
||||
}
|
||||
|
||||
_, err = client.BackupCRDs(context.Background(), tc.upgradeID)
|
||||
if tc.wantError {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
|
||||
data, err := afero.ReadFile(memFs, filepath.Join(client.crdBackupFolder(tc.upgradeID), crd.Name+".yaml"))
|
||||
require.NoError(err)
|
||||
assert.YAMLEq(tc.expectedFile, string(data))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackupCRs(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
upgradeID string
|
||||
crd apiextensionsv1.CustomResourceDefinition
|
||||
resource unstructured.Unstructured
|
||||
expectedFile string
|
||||
getCRsError error
|
||||
wantError bool
|
||||
}{
|
||||
"success": {
|
||||
upgradeID: "1234",
|
||||
crd: apiextensionsv1.CustomResourceDefinition{
|
||||
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
|
||||
Names: apiextensionsv1.CustomResourceDefinitionNames{
|
||||
Plural: "foobars",
|
||||
},
|
||||
Group: "some.group",
|
||||
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
|
||||
{
|
||||
Name: "versionZero",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
resource: unstructured.Unstructured{Object: map[string]any{"metadata": map[string]any{"name": "foobar"}}},
|
||||
expectedFile: "metadata:\n name: foobar\n",
|
||||
},
|
||||
"api request fails": {
|
||||
upgradeID: "1234",
|
||||
crd: apiextensionsv1.CustomResourceDefinition{
|
||||
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
|
||||
Names: apiextensionsv1.CustomResourceDefinitionNames{
|
||||
Plural: "foobars",
|
||||
},
|
||||
Group: "some.group",
|
||||
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
|
||||
{
|
||||
Name: "versionZero",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getCRsError: errors.New("api error"),
|
||||
wantError: true,
|
||||
},
|
||||
"custom resource not found": {
|
||||
upgradeID: "1234",
|
||||
crd: apiextensionsv1.CustomResourceDefinition{
|
||||
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
|
||||
Names: apiextensionsv1.CustomResourceDefinitionNames{
|
||||
Plural: "foobars",
|
||||
},
|
||||
Group: "some.group",
|
||||
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
|
||||
{
|
||||
Name: "versionZero",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getCRsError: k8serrors.NewNotFound(schema.GroupResource{Group: "some.group"}, "foobars"),
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
memFs := afero.NewMemMapFs()
|
||||
|
||||
client := KubeCmd{
|
||||
kubectl: &stubKubectl{crs: []unstructured.Unstructured{tc.resource}, getCRsError: tc.getCRsError},
|
||||
fileHandler: file.NewHandler(memFs),
|
||||
log: stubLog{},
|
||||
}
|
||||
|
||||
err := client.BackupCRs(context.Background(), []apiextensionsv1.CustomResourceDefinition{tc.crd}, tc.upgradeID)
|
||||
if tc.wantError {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
|
||||
data, err := afero.ReadFile(memFs, filepath.Join(client.backupFolder(tc.upgradeID), tc.crd.Spec.Group, tc.crd.Spec.Versions[0].Name, tc.resource.GetNamespace(), tc.resource.GetKind(), tc.resource.GetName()+".yaml"))
|
||||
if tc.expectedFile == "" {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.YAMLEq(tc.expectedFile, string(data))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type stubLog struct{}
|
||||
|
||||
func (s stubLog) Debugf(_ string, _ ...any) {}
|
||||
func (s stubLog) Sync() {}
|
||||
|
||||
func (c stubKubectl) ListCRDs(_ context.Context) ([]apiextensionsv1.CustomResourceDefinition, error) {
|
||||
if c.getCRDsError != nil {
|
||||
return nil, c.getCRDsError
|
||||
}
|
||||
return c.crds, nil
|
||||
}
|
||||
|
||||
func (c stubKubectl) ListCRs(_ context.Context, _ schema.GroupVersionResource) ([]unstructured.Unstructured, error) {
|
||||
if c.getCRsError != nil {
|
||||
return nil, c.getCRsError
|
||||
}
|
||||
return c.crs, nil
|
||||
}
|
|
@ -31,6 +31,7 @@ import (
|
|||
"github.com/edgelesssys/constellation/v2/internal/compatibility"
|
||||
"github.com/edgelesssys/constellation/v2/internal/config"
|
||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||
"github.com/edgelesssys/constellation/v2/internal/file"
|
||||
"github.com/edgelesssys/constellation/v2/internal/imagefetcher"
|
||||
internalk8s "github.com/edgelesssys/constellation/v2/internal/kubernetes"
|
||||
"github.com/edgelesssys/constellation/v2/internal/kubernetes/kubectl"
|
||||
|
@ -68,11 +69,12 @@ type KubeCmd struct {
|
|||
kubectl kubectlInterface
|
||||
imageFetcher imageFetcher
|
||||
outWriter io.Writer
|
||||
fileHandler file.Handler
|
||||
log debugLog
|
||||
}
|
||||
|
||||
// New returns a new KubeCmd.
|
||||
func New(outWriter io.Writer, kubeConfigPath string, log debugLog) (*KubeCmd, error) {
|
||||
func New(outWriter io.Writer, kubeConfigPath string, fileHandler file.Handler, log debugLog) (*KubeCmd, error) {
|
||||
client, err := kubectl.NewFromConfig(kubeConfigPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating kubectl client: %w", err)
|
||||
|
@ -80,6 +82,7 @@ func New(outWriter io.Writer, kubeConfigPath string, log debugLog) (*KubeCmd, er
|
|||
|
||||
return &KubeCmd{
|
||||
kubectl: client,
|
||||
fileHandler: fileHandler,
|
||||
imageFetcher: imagefetcher.New(),
|
||||
outWriter: outWriter,
|
||||
log: log,
|
||||
|
@ -506,6 +509,7 @@ type kubectlInterface interface {
|
|||
KubernetesVersion() (string, error)
|
||||
GetCR(ctx context.Context, gvr schema.GroupVersionResource, name string) (*unstructured.Unstructured, error)
|
||||
UpdateCR(ctx context.Context, gvr schema.GroupVersionResource, obj *unstructured.Unstructured) (*unstructured.Unstructured, error)
|
||||
crdLister
|
||||
}
|
||||
|
||||
type debugLog interface {
|
||||
|
|
|
@ -27,6 +27,7 @@ import (
|
|||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
@ -616,6 +617,10 @@ type stubKubectl struct {
|
|||
k8sErr error
|
||||
nodes []corev1.Node
|
||||
nodesErr error
|
||||
crds []apiextensionsv1.CustomResourceDefinition
|
||||
getCRDsError error
|
||||
crs []unstructured.Unstructured
|
||||
getCRsError error
|
||||
}
|
||||
|
||||
func (s *stubKubectl) GetConfigMap(_ context.Context, _, name string) (*corev1.ConfigMap, error) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue