diff --git a/.golangci.yaml b/.golangci.yaml index f14146f..26ae246 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -74,7 +74,7 @@ linters: settings: dupword: # Do not choke on SQL statements like `INSERT INTO things (foo, bar, baz) VALUES (TRUE, TRUE, TRUE)`. - ignore: [ "TRUE", "FALSE", "NULL" ] + ignore: ["TRUE", "FALSE", "NULL"] errcheck: check-type-assertions: false # Report about assignment of errors to blank identifier. @@ -103,7 +103,7 @@ linters: min-occurrences: 5 ignore-tests: true ignore-string-values: - - '^[a-zA-Z_-]{1,16}$' # ignore short identifiers like "account" or "project_id" + - "^[a-zA-Z_-]{1,16}$" # ignore short identifiers like "account" or "project_id" gocritic: enabled-checks: - boolExprSimplify @@ -136,7 +136,7 @@ linters: # for github.com/sapcc/vpa_butler - k8s.io/client-go toolchain-forbidden: true - go-version-pattern: 1\.\d+(\.0)?$ + go-version-pattern: 1\.\d+(\.\d+)?$ gosec: excludes: # gosec wants us to set a short ReadHeaderTimeout to avoid Slowloris attacks, but doing so would expose us to Keep-Alive race conditions (see https://iximiuz.com/en/posts/reverse-proxy-http-keep-alive-and-502s/ diff --git a/.typos.toml b/.typos.toml index 7dd0e8b..49ffe51 100644 --- a/.typos.toml +++ b/.typos.toml @@ -3,6 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 [default.extend-words] +ded = "ded" [files] extend-exclude = [ diff --git a/Makefile.maker.yaml b/Makefile.maker.yaml index 5e2c63d..147e9e7 100644 --- a/Makefile.maker.yaml +++ b/Makefile.maker.yaml @@ -4,9 +4,9 @@ metadata: url: https://github.com/cobaltcore-dev/cloud-profile-sync binaries: - - name: cloud-profile-sync + - name: cloud-profile-sync fromPackage: . - installTo: bin/ + installTo: bin/ golang: setGoModVersion: true @@ -35,12 +35,16 @@ renovate: reuse: annotations: - - paths: - - '**/zz_generated.deepcopy.go' - - 'crd/*' - SPDX-FileCopyrightText: 2020 The Kubernetes Authors # this is the copyright line for kubebuilder which generates those files - SPDX-License-Identifier: Apache-2.0 + - paths: + - "**/zz_generated.deepcopy.go" + - "crd/*" + SPDX-FileCopyrightText: 2020 The Kubernetes Authors # this is the copyright line for kubebuilder which generates those files + SPDX-License-Identifier: Apache-2.0 verbatim: | gardener-crds: install-controller-gen @controller-gen crd paths="github.com/gardener/gardener/pkg/apis/core/v1beta1" output:crd:artifacts:config=crd + +typos: + extendWords: + ded: ded diff --git a/cloudprofilesync/imageupdater.go b/cloudprofilesync/imageupdater.go index 85d610f..4914455 100644 --- a/cloudprofilesync/imageupdater.go +++ b/cloudprofilesync/imageupdater.go @@ -86,6 +86,7 @@ func (iu *ImageUpdater) Update(ctx context.Context, cpSpec *gardenerv1beta1.Clou } for _, sourceImage := range sourceImages { + supportInPlaceUpdate := slices.Contains(sourceImage.Capabilities[FeatureCapability], USIFeature) // Always write the full tag version (legacy path, safe for running Shoots). if idx, exists := existingVersions[sourceImage.Version]; exists { image.Versions[idx].Architectures = sourceImage.Architectures @@ -103,6 +104,11 @@ func (iu *ImageUpdater) Update(ctx context.Context, cpSpec *gardenerv1beta1.Clou }, Architectures: sourceImage.Architectures, }) + if supportInPlaceUpdate { + image.Versions[len(image.Versions)-1].InPlaceUpdates = &gardenerv1beta1.InPlaceUpdates{ + Supported: supportInPlaceUpdate, + } + } existingVersions[sourceImage.Version] = len(image.Versions) - 1 } } @@ -116,6 +122,11 @@ func (iu *ImageUpdater) Update(ctx context.Context, cpSpec *gardenerv1beta1.Clou existing.Architectures = append(existing.Architectures, arch) } } + if supportInPlaceUpdate { + existing.InPlaceUpdates = &gardenerv1beta1.InPlaceUpdates{ + Supported: supportInPlaceUpdate, + } + } } else { image.Versions = append(image.Versions, gardenerv1beta1.MachineImageVersion{ ExpirableVersion: gardenerv1beta1.ExpirableVersion{ @@ -123,6 +134,11 @@ func (iu *ImageUpdater) Update(ctx context.Context, cpSpec *gardenerv1beta1.Clou }, Architectures: slices.Clone(sourceImage.Architectures), }) + if supportInPlaceUpdate { + image.Versions[len(image.Versions)-1].InPlaceUpdates = &gardenerv1beta1.InPlaceUpdates{ + Supported: supportInPlaceUpdate, + } + } existingVersions[sourceImage.CleanVersion] = len(image.Versions) - 1 } } diff --git a/cloudprofilesync/imageupdater_test.go b/cloudprofilesync/imageupdater_test.go index 2b1bc35..e3faa9a 100644 --- a/cloudprofilesync/imageupdater_test.go +++ b/cloudprofilesync/imageupdater_test.go @@ -224,6 +224,25 @@ var _ = Describe("ImageUpdater", func() { Expect(json.Unmarshal(cpSpec.ProviderConfig.Raw, &fromProvider)).To(Succeed()) Expect(fromProvider).To(Equal(mockSource.images)) }) + + It("in-place update support", func(ctx SpecContext) { + mockSource.images = []cloudprofilesync.SourceImage{{ + Version: "1.0.0", + Architectures: []string{"amd64"}, + Capabilities: map[string]gardencorev1beta1.CapabilityValues{"feature": {cloudprofilesync.USIFeature}}}, + } + updater := cloudprofilesync.ImageUpdater{ + Log: logr.Discard(), + Source: &mockSource, + ImageName: "test", + } + var cpSpec gardencorev1beta1.CloudProfileSpec + Expect(updater.Update(ctx, &cpSpec)).To(Succeed()) + Expect(cpSpec.MachineImages[0].Versions).To(HaveLen(1)) + Expect(cpSpec.MachineImages[0].Versions[0].Version).To(Equal("1.0.0")) + Expect(cpSpec.MachineImages[0].Versions[0].InPlaceUpdates.Supported).To(BeTrue()) + }) + }) Describe("flag ON (dual-write clean version)", func() { @@ -249,6 +268,7 @@ var _ = Describe("ImageUpdater", func() { versions := cpSpec.MachineImages[0].Versions versionStrings := []string{versions[0].Version, versions[1].Version} Expect(versionStrings).To(ContainElements("2254.0.0-baremetal-sci-usi-amd64", "2254.0.0")) + Expect(versions[0].InPlaceUpdates.Supported).To(BeTrue()) }) It("does not add a duplicate clean version entry on re-reconcile", func(ctx SpecContext) { @@ -318,5 +338,29 @@ var _ = Describe("ImageUpdater", func() { Expect(cpSpec.MachineImages[0].Versions).To(HaveLen(1)) Expect(cpSpec.MachineImages[0].Versions[0].Version).To(Equal("1877.0.0")) }) + + It("in-place update support", func(ctx SpecContext) { + mockSource.images = []cloudprofilesync.SourceImage{{ + Version: "1.0.0", + CleanVersion: "1.1", + Architectures: []string{"amd64"}, + Capabilities: map[string]gardencorev1beta1.CapabilityValues{"feature": {cloudprofilesync.USIFeature}}}, + } + updater := cloudprofilesync.ImageUpdater{ + Log: logr.Discard(), + Source: &mockSource, + ImageName: "test", + EnableCapabilities: true, + } + var cpSpec gardencorev1beta1.CloudProfileSpec + Expect(updater.Update(ctx, &cpSpec)).To(Succeed()) + Expect(cpSpec.MachineImages[0].Versions).To(HaveLen(2)) + Expect(cpSpec.MachineImages[0].Versions[0].Version).To(Equal("1.0.0")) + Expect(cpSpec.MachineImages[0].Versions[0].InPlaceUpdates).NotTo(BeNil()) + Expect(cpSpec.MachineImages[0].Versions[0].InPlaceUpdates.Supported).To(BeTrue()) + Expect(cpSpec.MachineImages[0].Versions[1].Version).To(Equal("1.1.0")) + Expect(cpSpec.MachineImages[0].Versions[1].InPlaceUpdates).NotTo(BeNil()) + Expect(cpSpec.MachineImages[0].Versions[1].InPlaceUpdates.Supported).To(BeTrue()) + }) }) }) diff --git a/cloudprofilesync/source.go b/cloudprofilesync/source.go index 15f6d1b..68fd93a 100644 --- a/cloudprofilesync/source.go +++ b/cloudprofilesync/source.go @@ -18,15 +18,32 @@ import ( "oras.land/oras-go/v2/registry/remote/retry" ) +const ( + // ChostFeature represent having containerd + ChostFeature = "chost" + // PXEFeature represent pxe boot build + PXEFeature = "_pxe" + SCIFeature = "sci" + SCIBaseFeature = "scibase" + // CAPIFeature includes server, khost, and PXE; excludes SELinux and firewall + CAPIFeature = "capi" + // USIFeature shows UEFI build + USIFeature = "_usi" + USIDevFeature = "_usidev" + + ArchitectureCapability = "architecture" + FeatureCapability = "feature" +) + // validFeatureValues is the allowlist of feature values extracted from the feature_set annotation. var validFeatureValues = map[string]struct{}{ - "chost": {}, - "_pxe": {}, - "sci": {}, - "capi": {}, - "scibase": {}, - "_usi": {}, - "_usidev": {}, + ChostFeature: {}, + PXEFeature: {}, + SCIFeature: {}, + SCIBaseFeature: {}, + CAPIFeature: {}, + USIFeature: {}, + USIDevFeature: {}, } func filterFeatureSet(featureSet string) []string { @@ -161,8 +178,8 @@ func (o *OCI) GetVersions(ctx context.Context) ([]SourceImage, error) { features := filterFeatureSet(featureSet) if len(features) > 0 { capabilities = gardencorev1beta1.Capabilities{ - "architecture": {arch}, - "feature": features, + ArchitectureCapability: {arch}, + FeatureCapability: features, } cleanVersion = version } diff --git a/go.mod b/go.mod index aafcb04..0bc82ee 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/cobaltcore-dev/cloud-profile-sync -go 1.26 +go 1.26.2 require ( github.com/blang/semver/v4 v4.0.0