mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-12-15 08:05:19 -05:00
api: add functions to transparently handle signatures upon API interaction (#2142)
This commit is contained in:
parent
002c3a9a32
commit
dac690656e
45 changed files with 707 additions and 472 deletions
|
|
@ -80,6 +80,7 @@ go_library(
|
|||
"//internal/retry",
|
||||
"//internal/semver",
|
||||
"//internal/sigstore",
|
||||
"//internal/sigstore/keyselect",
|
||||
"//internal/versions",
|
||||
"//operators/constellation-node-operator/api/v1alpha1",
|
||||
"//verify/verifyproto",
|
||||
|
|
@ -162,6 +163,7 @@ go_test(
|
|||
"//internal/license",
|
||||
"//internal/logger",
|
||||
"//internal/semver",
|
||||
"//internal/sigstore",
|
||||
"//internal/versions",
|
||||
"//operators/constellation-node-operator/api/v1alpha1",
|
||||
"//verify/verifyproto",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import (
|
|||
"github.com/edgelesssys/constellation/v2/internal/config"
|
||||
"github.com/edgelesssys/constellation/v2/internal/file"
|
||||
"github.com/edgelesssys/constellation/v2/internal/sigstore"
|
||||
"github.com/edgelesssys/constellation/v2/internal/sigstore/keyselect"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
|
@ -69,11 +70,11 @@ func runConfigFetchMeasurements(cmd *cobra.Command, _ []string) error {
|
|||
cfm := &configFetchMeasurementsCmd{log: log, canFetchMeasurements: featureset.CanFetchMeasurements}
|
||||
|
||||
fetcher := attestationconfigapi.NewFetcherWithClient(http.DefaultClient)
|
||||
return cfm.configFetchMeasurements(cmd, sigstore.CosignVerifier{}, rekor, fileHandler, fetcher, http.DefaultClient)
|
||||
return cfm.configFetchMeasurements(cmd, sigstore.NewCosignVerifier, rekor, fileHandler, fetcher, http.DefaultClient)
|
||||
}
|
||||
|
||||
func (cfm *configFetchMeasurementsCmd) configFetchMeasurements(
|
||||
cmd *cobra.Command, cosign cosignVerifier, rekor rekorVerifier,
|
||||
cmd *cobra.Command, newCosignVerifier cosignVerifierConstructor, rekor rekorVerifier,
|
||||
fileHandler file.Handler, fetcher attestationconfigapi.Fetcher, client *http.Client,
|
||||
) error {
|
||||
flags, err := cfm.parseFetchMeasurementsFlags(cmd)
|
||||
|
|
@ -117,6 +118,15 @@ func (cfm *configFetchMeasurementsCmd) configFetchMeasurements(
|
|||
return err
|
||||
}
|
||||
|
||||
publicKey, err := keyselect.CosignPublicKeyForVersion(imageVersion)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting public key: %w", err)
|
||||
}
|
||||
cosign, err := newCosignVerifier(publicKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating cosign verifier: %w", err)
|
||||
}
|
||||
|
||||
var fetchedMeasurements measurements.M
|
||||
var hash string
|
||||
if flags.insecure {
|
||||
|
|
@ -147,7 +157,7 @@ func (cfm *configFetchMeasurementsCmd) configFetchMeasurements(
|
|||
return fmt.Errorf("fetching and verifying measurements: %w", err)
|
||||
}
|
||||
cfm.log.Debugf("Fetched and verified measurements, hash is %s", hash)
|
||||
if err := sigstore.VerifyWithRekor(cmd.Context(), imageVersion, rekor, hash); err != nil {
|
||||
if err := sigstore.VerifyWithRekor(cmd.Context(), publicKey, rekor, hash); err != nil {
|
||||
cmd.PrintErrf("Ignoring Rekor related error: %v\n", err)
|
||||
cmd.PrintErrln("Make sure the downloaded measurements are trustworthy!")
|
||||
}
|
||||
|
|
@ -244,6 +254,4 @@ type rekorVerifier interface {
|
|||
VerifyEntry(context.Context, string, string) error
|
||||
}
|
||||
|
||||
type cosignVerifier interface {
|
||||
VerifySignature(content, signature, publicKey []byte) error
|
||||
}
|
||||
type cosignVerifierConstructor func([]byte) (sigstore.Verifier, error)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import (
|
|||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||
"github.com/edgelesssys/constellation/v2/internal/file"
|
||||
"github.com/edgelesssys/constellation/v2/internal/logger"
|
||||
"github.com/edgelesssys/constellation/v2/internal/sigstore"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -102,12 +103,11 @@ func TestParseFetchMeasurementsFlags(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestUpdateURLs(t *testing.T) {
|
||||
ver := versionsapi.Version{
|
||||
Ref: "foo",
|
||||
Stream: "nightly",
|
||||
Version: "v7.7.7",
|
||||
Kind: versionsapi.VersionKindImage,
|
||||
}
|
||||
require := require.New(t)
|
||||
|
||||
ver, err := versionsapi.NewVersion("foo", "nightly", "v7.7.7", versionsapi.VersionKindImage)
|
||||
require.NoError(err)
|
||||
|
||||
testCases := map[string]struct {
|
||||
conf *config.Config
|
||||
flags *fetchMeasurementsFlags
|
||||
|
|
@ -234,39 +234,43 @@ func TestConfigFetchMeasurements(t *testing.T) {
|
|||
})
|
||||
|
||||
testCases := map[string]struct {
|
||||
cosign cosignVerifier
|
||||
cosign cosignVerifierConstructor
|
||||
rekor rekorVerifier
|
||||
insecureFlag bool
|
||||
wantErr bool
|
||||
}{
|
||||
"success": {
|
||||
cosign: &stubCosignVerifier{},
|
||||
cosign: newStubCosignVerifier,
|
||||
rekor: singleUUIDVerifier(),
|
||||
},
|
||||
"success without cosign": {
|
||||
insecureFlag: true,
|
||||
cosign: &stubCosignVerifier{
|
||||
verifyError: assert.AnError,
|
||||
cosign: func(_ []byte) (sigstore.Verifier, error) {
|
||||
return &stubCosignVerifier{
|
||||
verifyError: assert.AnError,
|
||||
}, nil
|
||||
},
|
||||
rekor: singleUUIDVerifier(),
|
||||
},
|
||||
"failing search should not result in error": {
|
||||
cosign: &stubCosignVerifier{},
|
||||
cosign: newStubCosignVerifier,
|
||||
rekor: &stubRekorVerifier{
|
||||
SearchByHashUUIDs: []string{},
|
||||
SearchByHashError: assert.AnError,
|
||||
},
|
||||
},
|
||||
"failing verify should not result in error": {
|
||||
cosign: &stubCosignVerifier{},
|
||||
cosign: newStubCosignVerifier,
|
||||
rekor: &stubRekorVerifier{
|
||||
SearchByHashUUIDs: []string{"11111111111111111111111111111111111111111111111111111111111111111111111111111111"},
|
||||
VerifyEntryError: assert.AnError,
|
||||
},
|
||||
},
|
||||
"signature verification failure": {
|
||||
cosign: &stubCosignVerifier{
|
||||
verifyError: assert.AnError,
|
||||
cosign: func(_ []byte) (sigstore.Verifier, error) {
|
||||
return &stubCosignVerifier{
|
||||
verifyError: assert.AnError,
|
||||
}, nil
|
||||
},
|
||||
rekor: singleUUIDVerifier(),
|
||||
wantErr: true,
|
||||
|
|
|
|||
|
|
@ -307,7 +307,7 @@ func validateCLIandConstellationVersionAreEqual(cliVersion semver.Semver, imageV
|
|||
return fmt.Errorf("parsing image version: %w", err)
|
||||
}
|
||||
|
||||
semImage, err := semver.New(parsedImageVersion.Version)
|
||||
semImage, err := semver.New(parsedImageVersion.Version())
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing image semantical version: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import (
|
|||
"github.com/edgelesssys/constellation/v2/internal/kubernetes/kubectl"
|
||||
consemver "github.com/edgelesssys/constellation/v2/internal/semver"
|
||||
"github.com/edgelesssys/constellation/v2/internal/sigstore"
|
||||
"github.com/edgelesssys/constellation/v2/internal/sigstore/keyselect"
|
||||
"github.com/edgelesssys/constellation/v2/internal/versions"
|
||||
"github.com/siderolabs/talos/pkg/machinery/config/encoder"
|
||||
"github.com/spf13/afero"
|
||||
|
|
@ -86,7 +87,6 @@ func runUpgradeCheck(cmd *cobra.Command, _ []string) error {
|
|||
verListFetcher: versionfetcher,
|
||||
fileHandler: fileHandler,
|
||||
client: http.DefaultClient,
|
||||
cosign: sigstore.CosignVerifier{},
|
||||
rekor: rekor,
|
||||
flags: flags,
|
||||
cliVersion: constants.BinaryVersion(),
|
||||
|
|
@ -301,7 +301,7 @@ func sortedMapKeys[T any](a map[string]T) []string {
|
|||
func filterImageUpgrades(currentVersion string, newVersions []versionsapi.Version) []versionsapi.Version {
|
||||
newImages := []versionsapi.Version{}
|
||||
for i := range newVersions {
|
||||
if err := compatibility.IsValidUpgrade(currentVersion, newVersions[i].Version); err != nil {
|
||||
if err := compatibility.IsValidUpgrade(currentVersion, newVersions[i].Version()); err != nil {
|
||||
continue
|
||||
}
|
||||
newImages = append(newImages, newVersions[i])
|
||||
|
|
@ -338,7 +338,6 @@ type versionCollector struct {
|
|||
verListFetcher versionListFetcher
|
||||
fileHandler file.Handler
|
||||
client *http.Client
|
||||
cosign cosignVerifier
|
||||
rekor rekorVerifier
|
||||
flags upgradeCheckFlags
|
||||
versionsapi versionFetcher
|
||||
|
|
@ -346,11 +345,30 @@ type versionCollector struct {
|
|||
log debugLog
|
||||
}
|
||||
|
||||
func (v *versionCollector) newMeasurements(ctx context.Context, csp cloudprovider.Provider, attestationVariant variant.Variant, images []versionsapi.Version) (map[string]measurements.M, error) {
|
||||
func (v *versionCollector) newMeasurements(ctx context.Context, csp cloudprovider.Provider, attestationVariant variant.Variant, versions []versionsapi.Version) (map[string]measurements.M, error) {
|
||||
// get expected measurements for each image
|
||||
upgrades, err := getCompatibleImageMeasurements(ctx, v.writer, v.client, v.cosign, v.rekor, csp, attestationVariant, images, v.log)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching measurements for compatible images: %w", err)
|
||||
upgrades := make(map[string]measurements.M)
|
||||
for _, version := range versions {
|
||||
v.log.Debugf("Fetching measurements for image: %s", version)
|
||||
shortPath := version.ShortPath()
|
||||
|
||||
publicKey, err := keyselect.CosignPublicKeyForVersion(version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting public key: %w", err)
|
||||
}
|
||||
cosign, err := sigstore.NewCosignVerifier(publicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("setting public key: %w", err)
|
||||
}
|
||||
|
||||
measurements, err := getCompatibleImageMeasurements(ctx, v.writer, v.client, cosign, v.rekor, csp, attestationVariant, version, v.log)
|
||||
if err != nil {
|
||||
if _, err := fmt.Fprintf(v.writer, "Skipping compatible image %q: %s\n", shortPath, err); err != nil {
|
||||
return nil, fmt.Errorf("writing to buffer: %w", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
upgrades[shortPath] = measurements
|
||||
}
|
||||
v.log.Debugf("Compatible image measurements are %v", upgrades)
|
||||
|
||||
|
|
@ -609,49 +627,41 @@ func getCurrentKubernetesVersion(ctx context.Context, checker upgradeChecker) (s
|
|||
}
|
||||
|
||||
// getCompatibleImageMeasurements retrieves the expected measurements for each image.
|
||||
func getCompatibleImageMeasurements(ctx context.Context, writer io.Writer, client *http.Client, cosign cosignVerifier, rekor rekorVerifier,
|
||||
csp cloudprovider.Provider, attestationVariant variant.Variant, versions []versionsapi.Version, log debugLog,
|
||||
) (map[string]measurements.M, error) {
|
||||
upgrades := make(map[string]measurements.M)
|
||||
for _, version := range versions {
|
||||
log.Debugf("Fetching measurements for image: %s", version)
|
||||
shortPath := version.ShortPath()
|
||||
measurementsURL, signatureURL, err := versionsapi.MeasurementURL(version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var fetchedMeasurements measurements.M
|
||||
log.Debugf("Fetching for measurement url: %s", measurementsURL)
|
||||
hash, err := fetchedMeasurements.FetchAndVerify(
|
||||
ctx, client, cosign,
|
||||
measurementsURL,
|
||||
signatureURL,
|
||||
version,
|
||||
csp,
|
||||
attestationVariant,
|
||||
)
|
||||
if err != nil {
|
||||
if _, err := fmt.Fprintf(writer, "Skipping compatible image %q: %s\n", shortPath, err); err != nil {
|
||||
return nil, fmt.Errorf("writing to buffer: %w", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err = sigstore.VerifyWithRekor(ctx, version, rekor, hash); err != nil {
|
||||
if _, err := fmt.Fprintf(writer, "Warning: Unable to verify '%s' in Rekor.\n", hash); err != nil {
|
||||
return nil, fmt.Errorf("writing to buffer: %w", err)
|
||||
}
|
||||
if _, err := fmt.Fprintf(writer, "Make sure measurements are correct.\n"); err != nil {
|
||||
return nil, fmt.Errorf("writing to buffer: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
upgrades[shortPath] = fetchedMeasurements
|
||||
|
||||
func getCompatibleImageMeasurements(ctx context.Context, writer io.Writer, client *http.Client, cosign sigstore.Verifier, rekor rekorVerifier,
|
||||
csp cloudprovider.Provider, attestationVariant variant.Variant, version versionsapi.Version, log debugLog,
|
||||
) (measurements.M, error) {
|
||||
measurementsURL, signatureURL, err := versionsapi.MeasurementURL(version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return upgrades, nil
|
||||
var fetchedMeasurements measurements.M
|
||||
log.Debugf("Fetching for measurement url: %s", measurementsURL)
|
||||
|
||||
hash, err := fetchedMeasurements.FetchAndVerify(
|
||||
ctx, client, cosign,
|
||||
measurementsURL,
|
||||
signatureURL,
|
||||
version,
|
||||
csp,
|
||||
attestationVariant,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching measurements: %w", err)
|
||||
}
|
||||
|
||||
pubkey, err := keyselect.CosignPublicKeyForVersion(version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting public key: %w", err)
|
||||
}
|
||||
|
||||
if err = sigstore.VerifyWithRekor(ctx, pubkey, rekor, hash); err != nil {
|
||||
if _, err := fmt.Fprintf(writer, "Warning: Unable to verify '%s' in Rekor.\nMake sure measurements are correct.\n", hash); err != nil {
|
||||
return nil, fmt.Errorf("writing to buffer: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return fetchedMeasurements, nil
|
||||
}
|
||||
|
||||
type versionFetcher interface {
|
||||
|
|
|
|||
|
|
@ -151,32 +151,23 @@ func TestGetCurrentImageVersion(t *testing.T) {
|
|||
|
||||
func TestGetCompatibleImageMeasurements(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
csp := cloudprovider.Azure
|
||||
attestationVariant := variant.AzureSEVSNP{}
|
||||
zero := versionsapi.Version{
|
||||
Ref: "-",
|
||||
Stream: "stable",
|
||||
Version: "v0.0.0",
|
||||
Kind: versionsapi.VersionKindImage,
|
||||
}
|
||||
one := versionsapi.Version{
|
||||
Ref: "-",
|
||||
Stream: "stable",
|
||||
Version: "v1.0.0",
|
||||
Kind: versionsapi.VersionKindImage,
|
||||
}
|
||||
images := []versionsapi.Version{zero, one}
|
||||
|
||||
versionZero, err := versionsapi.NewVersion("-", "stable", "v0.0.0", versionsapi.VersionKindImage)
|
||||
require.NoError(err)
|
||||
|
||||
client := newTestClient(func(req *http.Request) *http.Response {
|
||||
if strings.HasSuffix(req.URL.String(), "v0.0.0/azure/measurements.json") {
|
||||
if strings.HasSuffix(req.URL.String(), "v0.0.0/image/measurements.json") {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader(`{"csp":"azure","image":"v0.0.0","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`)),
|
||||
Body: io.NopCloser(strings.NewReader(`{"version": "v0.0.0","ref": "-","stream": "stable","list": [{"csp": "Azure","attestationVariant": "azure-sev-snp","measurements": {"0": {"expected": "0000000000000000000000000000000000000000000000000000000000000000","warnOnly": false}}}]}`)),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(req.URL.String(), "v0.0.0/azure/measurements.json.sig") {
|
||||
if strings.HasSuffix(req.URL.String(), "v0.0.0/image/measurements.json.sig") {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader("MEQCIGRR7RaSMs892Ta06/Tz7LqPUxI05X4wQcP+nFFmZtmaAiBNl9X8mUKmUBfxg13LQBfmmpw6JwYQor5hOwM3NFVPAg==")),
|
||||
|
|
@ -184,21 +175,6 @@ func TestGetCompatibleImageMeasurements(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
if strings.HasSuffix(req.URL.String(), "v1.0.0/azure/measurements.json") {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader(`{"csp":"azure","image":"v1.0.0","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`)),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(req.URL.String(), "v1.0.0/azure/measurements.json.sig") {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader("MEQCIFh8CVELp/Da2U2Jt404OXsUeDfqtrf3pqGRuvxnxhI8AiBTHF9tHEPwFedYG3Jgn2ELOxss+Ybc6135vEtClBrbpg==")),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Body: io.NopCloser(strings.NewReader("Not found.")),
|
||||
|
|
@ -206,7 +182,7 @@ func TestGetCompatibleImageMeasurements(t *testing.T) {
|
|||
}
|
||||
})
|
||||
|
||||
upgrades, err := getCompatibleImageMeasurements(context.Background(), &bytes.Buffer{}, client, &stubCosignVerifier{}, singleUUIDVerifier(), csp, attestationVariant, images, logger.NewTest(t))
|
||||
upgrades, err := getCompatibleImageMeasurements(context.Background(), &bytes.Buffer{}, client, &stubCosignVerifier{}, singleUUIDVerifier(), csp, attestationVariant, versionZero, logger.NewTest(t))
|
||||
assert.NoError(err)
|
||||
|
||||
for _, measurement := range upgrades {
|
||||
|
|
@ -215,18 +191,13 @@ func TestGetCompatibleImageMeasurements(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestUpgradeCheck(t *testing.T) {
|
||||
v2_3 := versionsapi.Version{
|
||||
Ref: "-",
|
||||
Stream: "stable",
|
||||
Version: "v2.3.0",
|
||||
Kind: versionsapi.VersionKindImage,
|
||||
}
|
||||
v2_5 := versionsapi.Version{
|
||||
Ref: "-",
|
||||
Stream: "stable",
|
||||
Version: "v2.5.0",
|
||||
Kind: versionsapi.VersionKindImage,
|
||||
}
|
||||
require := require.New(t)
|
||||
v2_3, err := versionsapi.NewVersion("-", "stable", "v2.3.0", versionsapi.VersionKindImage)
|
||||
require.NoError(err)
|
||||
|
||||
v2_5, err := versionsapi.NewVersion("-", "stable", "v2.5.0", versionsapi.VersionKindImage)
|
||||
require.NoError(err)
|
||||
|
||||
collector := stubVersionCollector{
|
||||
supportedServicesVersions: consemver.NewFromInt(2, 5, 0, ""),
|
||||
supportedImages: []versionsapi.Version{v2_3},
|
||||
|
|
@ -279,7 +250,6 @@ func TestUpgradeCheck(t *testing.T) {
|
|||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
fileHandler := file.NewHandler(afero.NewMemMapFs())
|
||||
cfg := defaultConfigWithExpectedMeasurements(t, config.Default(), tc.csp)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
package cmd
|
||||
|
||||
import "context"
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/sigstore"
|
||||
)
|
||||
|
||||
// singleUUIDVerifier constructs a RekorVerifier that returns a single UUID and no errors,
|
||||
// and should work for most tests on the happy path.
|
||||
|
|
@ -39,6 +43,10 @@ type stubCosignVerifier struct {
|
|||
verifyError error
|
||||
}
|
||||
|
||||
func (v *stubCosignVerifier) VerifySignature(_, _, _ []byte) error {
|
||||
func newStubCosignVerifier(_ []byte) (sigstore.Verifier, error) {
|
||||
return &stubCosignVerifier{}, nil
|
||||
}
|
||||
|
||||
func (v *stubCosignVerifier) VerifySignature(_, _ []byte) error {
|
||||
return v.verifyError
|
||||
}
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ func (u *Upgrader) UpgradeNodeVersion(ctx context.Context, conf *config.Config,
|
|||
upgradeErrs := []error{}
|
||||
var upgradeErr *compatibility.InvalidUpgradeError
|
||||
|
||||
err = u.updateImage(&nodeVersion, imageReference, imageVersion.Version, force)
|
||||
err = u.updateImage(&nodeVersion, imageReference, imageVersion.Version(), force)
|
||||
switch {
|
||||
case errors.As(err, &upgradeErr):
|
||||
upgradeErrs = append(upgradeErrs, fmt.Errorf("skipping image upgrades: %w", err))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue