From 7d5cf6c28951ec426371a83224ded1ac93f2847d Mon Sep 17 00:00:00 2001 From: Kurt Garloff Date: Sat, 25 Apr 2026 16:03:00 +0200 Subject: [PATCH 1/3] Allow for dashes in clusterstack names. We do this because the mere existence of a openstack-hosted-control-plane-1-34-v0-sha-0ewkztd tag in the registry confused csctl and prevented us from releasing a new cluster stack. So, we tolerate dashes in clusterstack names now by being more robust in parsing, So, if we don't have '-v' at the right position in the string spliited by '-', we look for a '-v' further to the right. We then assume that the extra dashes belong to the name and process accordingly. The string parsing code is not very beautiful, but has been tested successfully. This fixes https://github.com/SovereignCloudStack/csctl/issues/220. Signed-off-by: Kurt Garloff --- .../pkg/clusterstack/clusterstack.go | 74 ++++++++++++------- 1 file changed, 48 insertions(+), 26 deletions(-) diff --git a/vendor/github.com/SovereignCloudStack/cluster-stack-operator/pkg/clusterstack/clusterstack.go b/vendor/github.com/SovereignCloudStack/cluster-stack-operator/pkg/clusterstack/clusterstack.go index 47f5b99e..2f97afa1 100644 --- a/vendor/github.com/SovereignCloudStack/cluster-stack-operator/pkg/clusterstack/clusterstack.go +++ b/vendor/github.com/SovereignCloudStack/cluster-stack-operator/pkg/clusterstack/clusterstack.go @@ -59,13 +59,24 @@ var ( // e.g. - "docker-ferrol-1-27-v1", "docker-ferrol-1-27-v1-alpha.1", etc. func NewFromClusterClassProperties(str string) (ClusterStack, error) { splitted := strings.Split(str, Separator) - if len(splitted) != 5 && len(splitted) != 6 { + splen := len(splitted) + // search for rightmost -vX + offset := 0 + for ((4+offset < splen) && (splitted[4+offset][0] != 'v')) { + offset += 1 + } + if ((splen < 5+offset) || (4+offset == splen) || (splitted[4+offset][0] != 'v') || (splen > 6+offset)) { return ClusterStack{}, ErrInvalidFormat } - + var name string + if offset != 0 { + name = strings.Join(splitted[1:2+offset], Separator) + } else { + name = splitted[1] + } clusterStack := ClusterStack{ Provider: splitted[0], - Name: splitted[1], + Name: name, } if clusterStack.Provider == "" { @@ -78,18 +89,22 @@ func NewFromClusterClassProperties(str string) (ClusterStack, error) { var err error - clusterStack.KubernetesVersion, err = kubernetesversion.New(splitted[2], splitted[3]) + clusterStack.KubernetesVersion, err = kubernetesversion.New(splitted[2+offset], splitted[3+offset]) if err != nil { - return ClusterStack{}, fmt.Errorf("failed to create Kubernetes version from %s-%s: %w", splitted[2], splitted[3], err) + return ClusterStack{}, fmt.Errorf("failed to create Kubernetes version from %s-%s: %w", + splitted[2+offset], splitted[3+offset], err) } var versionString string - if len(splitted) == 5 { + if splen-offset == 5 { // e.g. myprovider-myclusterstack-1-26-v1 - versionString = splitted[4] - } else if len(splitted) == 6 { + versionString = splitted[4+offset] + } else if splen-offset == 6 { // e.g. myprovider-myclusterstack-1-26-v1-alpha.0 - versionString = strings.Join(splitted[4:6], Separator) + versionString = strings.Join(splitted[4+offset:6+offset], Separator) + } else { + // this should be impossible + panic("The impossible error") } // version string like v1-alpha.0 @@ -107,37 +122,44 @@ func NewFromClusterClassProperties(str string) (ClusterStack, error) { // e.g. - "docker-ferrol-1-27-v1", "docker-ferrol-1-27-v1-alpha-1", etc. func NewFromClusterStackReleaseProperties(str string) (ClusterStack, error) { splitted := strings.Split(str, Separator) - if len(splitted) != 5 && len(splitted) != 7 { + splen := len(splitted) + // search for rightmost -vX + offset := 0 + for ((4+offset < splen) && (splitted[4+offset][0] != 'v')) { + offset += 1 + } + if ((splen < 5+offset) || (4+offset == splen) || (splitted[4+offset][0] != 'v') || (splen > 7+offset)) { return ClusterStack{}, ErrInvalidFormat } - + var name string + if offset != 0 { + name = strings.Join(splitted[1:2+offset], Separator) + } else { + name = splitted[1] + } clusterStack := ClusterStack{ Provider: splitted[0], - Name: splitted[1], - } - - if clusterStack.Provider == "" { - return ClusterStack{}, ErrInvalidProvider - } - - if clusterStack.Name == "" { - return ClusterStack{}, ErrInvalidName + Name: name, } var err error - clusterStack.KubernetesVersion, err = kubernetesversion.New(splitted[2], splitted[3]) + clusterStack.KubernetesVersion, err = kubernetesversion.New(splitted[2+offset], splitted[3+offset]) if err != nil { - return ClusterStack{}, fmt.Errorf("failed to create Kubernetes version from %s-%s: %w", splitted[2], splitted[3], err) + return ClusterStack{}, fmt.Errorf("failed to create Kubernetes version from %s-%s: %w", + splitted[2+offset], splitted[3+offset], err) } var versionString string - if len(splitted) == 5 { + if splen-offset == 5 { // e.g. myprovider-myclusterstack-1-26-v1 - versionString = splitted[4] - } else if len(splitted) == 7 { + versionString = splitted[4+offset] + } else if splen-offset == 6 { + // e.g. myprovider-myclusterstack-1-26-v1-alpha + versionString = strings.Join(splitted[4+offset:6+offset], Separator) + } else if splen-offset == 7 { // e.g. myprovider-myclusterstack-1-26-v1-alpha-0 - versionString = strings.Join(splitted[4:7], Separator) + versionString = strings.Join(splitted[4+offset:7+offset], Separator) } // version string like v1-alpha-0 From 48c0e897c3385cb49f1d3669906354d5f78997dc Mon Sep 17 00:00:00 2001 From: Kurt Garloff Date: Wed, 6 May 2026 14:57:00 +0000 Subject: [PATCH 2/3] Add a hash version also for cluster class. This is the main thing and it should be hashed, as changes there are consequential. Signed-off-by: Kurt Garloff --- pkg/clusterstack/metadata.go | 1 + pkg/clusterstack/mode.go | 28 ++++++++++++++++++++++++++-- pkg/cmd/create.go | 10 +++++++++- pkg/cshash/hash.go | 9 ++++++++- 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/pkg/clusterstack/metadata.go b/pkg/clusterstack/metadata.go index 0141f480..3a6814a0 100644 --- a/pkg/clusterstack/metadata.go +++ b/pkg/clusterstack/metadata.go @@ -28,6 +28,7 @@ import ( type Component struct { ClusterAddon string `yaml:"clusterAddon"` NodeImage string `yaml:"nodeImage,omitempty"` + ClusterClass string `yaml:"clusterClass"` } // Versions contains version information. diff --git a/pkg/clusterstack/mode.go b/pkg/clusterstack/mode.go index 9db57dd8..0fff3a67 100644 --- a/pkg/clusterstack/mode.go +++ b/pkg/clusterstack/mode.go @@ -35,7 +35,9 @@ func HandleStableMode(gitHubReleasePath string, currentReleaseHash, latestReleas return nil, fmt.Errorf("failed to bump cluster stack: %w", err) } - if currentReleaseHash.ClusterAddonDir != latestReleaseHash.ClusterAddonDir || currentReleaseHash.ClusterAddonValues != latestReleaseHash.ClusterAddonValues { + if currentReleaseHash.ClusterAddonDir != latestReleaseHash.ClusterAddonDir || + currentReleaseHash.ClusterAddonValues != latestReleaseHash.ClusterAddonValues || + currentReleaseHash.ClusterClassDir != latestReleaseHash.ClusterClassDir { metadata.Versions.Components.ClusterAddon, err = BumpVersion(metadata.Versions.Components.ClusterAddon) if err != nil { return nil, fmt.Errorf("failed to bump cluster addon: %w", err) @@ -58,6 +60,22 @@ func HandleStableMode(gitHubReleasePath string, currentReleaseHash, latestReleas fmt.Printf("NodeImage Version unchanged: %s\n", metadata.Versions.Components.NodeImage) } } + if currentReleaseHash.ClusterClassDir != latestReleaseHash.ClusterClassDir { + metadata.Versions.Components.ClusterClass, err = BumpVersion(metadata.Versions.Components.ClusterClass) + if err != nil { + metadata.Versions.Components.ClusterClass = "v1" + fmt.Printf("Initial ClusterClass Version: %s\n", metadata.Versions.Components.ClusterClass) + //return nil, fmt.Errorf("failed to bump cluster class: %w", err) + } else { + fmt.Printf("Bumped ClusterClass Version: %s\n", metadata.Versions.Components.ClusterClass) + } + } else { + if metadata.Versions.Components.ClusterClass == "" { + fmt.Println("No ClusterClass Version.") + } else { + fmt.Printf("ClusterClass Version unchanged: %s\n", metadata.Versions.Components.ClusterClass) + } + } return metadata, nil } @@ -75,19 +93,24 @@ func HandleHashMode(currentRelease cshash.ReleaseHash, kubernetesVersion string) Components: Component{ ClusterAddon: clusterStackHash, NodeImage: clusterStackHash, + ClusterClass: clusterStackHash, }, }, } } // HandleCustomMode handles custom mode with version for all components. -func HandleCustomMode(kubernetesVersion, clusterStackVersion, clusterAddonVersion, nodeImageVersion string) (*MetaData, error) { +func HandleCustomMode(kubernetesVersion, clusterStackVersion, clusterAddonVersion, + clusterClassVersion, nodeImageVersion string) (*MetaData, error) { if _, err := version.New(clusterStackVersion); err != nil { return nil, fmt.Errorf("failed to verify custom version for cluster stack: %q: %w", clusterStackVersion, err) } if _, err := version.New(clusterAddonVersion); err != nil { return nil, fmt.Errorf("failed to verify custom version for cluster addon: %q: %w", clusterAddonVersion, err) } + if _, err := version.New(clusterClassVersion); err != nil { + return nil, fmt.Errorf("failed to verify custom version for cluster class: %q: %w", clusterClassVersion, err) + } if _, err := version.New(nodeImageVersion); err != nil { return nil, fmt.Errorf("failed to verify custom version for node image: %q: %w", nodeImageVersion, err) } @@ -99,6 +122,7 @@ func HandleCustomMode(kubernetesVersion, clusterStackVersion, clusterAddonVersio ClusterStack: clusterStackVersion, Components: Component{ ClusterAddon: clusterAddonVersion, + ClusterClass: clusterClassVersion, NodeImage: nodeImageVersion, }, }, diff --git a/pkg/cmd/create.go b/pkg/cmd/create.go index 90b0459c..72b7dffa 100644 --- a/pkg/cmd/create.go +++ b/pkg/cmd/create.go @@ -61,6 +61,7 @@ var ( nodeImageRegistry string clusterStackVersion string clusterAddonVersion string + clusterClassVersion string nodeImageVersion string remote string publish bool @@ -95,6 +96,7 @@ func init() { createCmd.Flags().StringVarP(&nodeImageRegistry, "node-image-registry", "r", "", "It defines the node image registry. For example oci://ghcr.io/foo/bar/node-images/staging/") createCmd.Flags().StringVar(&clusterStackVersion, "cluster-stack-version", "", "It is used to specify the semver version for the cluster stack in the custom mode") createCmd.Flags().StringVar(&clusterAddonVersion, "cluster-addon-version", "", "It is used to specify the semver version for the cluster addon in the custom mode") + createCmd.Flags().StringVar(&clusterClassVersion, "cluster-class-version", "", "It is used to specify the semver version for the cluster class in the custom mode") createCmd.Flags().StringVar(&nodeImageVersion, "node-image-version", "", "It is used to specify the semver version for the node images in the custom mode") createCmd.Flags().StringVar(&remote, "remote", "github", "Which remote repository to use and thus which credentials are required. Currently supported are 'github' and 'oci'.") createCmd.Flags().BoolVar(&publish, "publish", false, "Publish release after creation is done. This is only implemented for OCI currently.") @@ -168,6 +170,7 @@ func GetCreateOptions(ctx context.Context, clusterStackPath string) (*CreateOpti createOption.Metadata.Versions.ClusterStack = "v1" createOption.Metadata.Versions.Components.ClusterAddon = "v1" createOption.Metadata.Versions.Components.NodeImage = "v1" + createOption.Metadata.Versions.Components.ClusterClass = "v1" } else { if err := downloadReleaseAssets(ctx, latestRepoRelease, "./.tmp/release/", ac); err != nil { return nil, fmt.Errorf("failed to download release asset: %w", err) @@ -196,8 +199,12 @@ func GetCreateOptions(ctx context.Context, clusterStackPath string) (*CreateOpti if nodeImageVersion == "" { return nil, errors.New("please specify a semver for custom version with --node-image-version flag") } + if clusterClassVersion == "" { + return nil, errors.New("please specify a semver for custom version with --cluster-class-version flag") + } - createOption.Metadata, err = clusterstack.HandleCustomMode(createOption.Config.Config.KubernetesVersion, clusterStackVersion, clusterAddonVersion, nodeImageVersion) + createOption.Metadata, err = clusterstack.HandleCustomMode(createOption.Config.Config.KubernetesVersion, + clusterStackVersion, clusterAddonVersion, clusterClassVersion, nodeImageVersion) if err != nil { return nil, fmt.Errorf("failed to handle custom mode: %w", err) } @@ -257,6 +264,7 @@ func createAction(cmd *cobra.Command, args []string) error { func (c *CreateOptions) validateHash() error { if c.CurrentReleaseHash.ClusterAddonDir == c.LatestReleaseHash.ClusterAddonDir && c.CurrentReleaseHash.ClusterAddonValues == c.LatestReleaseHash.ClusterAddonValues && + c.CurrentReleaseHash.ClusterClassDir == c.LatestReleaseHash.ClusterClassDir && c.CurrentReleaseHash.NodeImageDir == c.LatestReleaseHash.NodeImageDir { return errors.New("no change in the cluster stack") } diff --git a/pkg/cshash/hash.go b/pkg/cshash/hash.go index f8ad01c6..2c659ba2 100644 --- a/pkg/cshash/hash.go +++ b/pkg/cshash/hash.go @@ -35,6 +35,7 @@ const ( clusterAddonDirName = "cluster-addon" nodeImageDirName = "node-image" clusterAddonValuesFileName = "cluster-addon-values.yaml" + clusterClassDirName = "cluster-class" ) // ReleaseHash contains the information of release hash. @@ -42,6 +43,7 @@ type ReleaseHash struct { ClusterStack string `json:"clusterStack"` ClusterAddonDir string `json:"clusterAddonDir"` ClusterAddonValues string `json:"clusterAddonValues"` + ClusterClassDir string `json:"clusterClassDir"` NodeImageDir string `json:"nodeImageDir,omitempty"` } @@ -80,7 +82,9 @@ func GetHash(path string) (ReleaseHash, error) { for _, entry := range entries { entryPath := filepath.Join(path, entry.Name()) - if entry.IsDir() && (entry.Name() == clusterAddonDirName || entry.Name() == nodeImageDirName) { + if entry.IsDir() && (entry.Name() == clusterAddonDirName || + entry.Name() == nodeImageDirName || + entry.Name() == clusterClassDirName) { hash, err := dirhash.HashDir(entryPath, "", dirhash.DefaultHash) if err != nil { return ReleaseHash{}, fmt.Errorf("failed to hash dir: %w", err) @@ -92,6 +96,8 @@ func GetHash(path string) (ReleaseHash, error) { releaseHash.ClusterAddonDir = hash case nodeImageDirName: releaseHash.NodeImageDir = hash + case clusterClassDirName: + releaseHash.ClusterClassDir = hash default: // Should not happen return ReleaseHash{}, fmt.Errorf("unknown name type %s", entryPath) @@ -114,6 +120,7 @@ func GetHash(path string) (ReleaseHash, error) { func (r ReleaseHash) ValidateWithLatestReleaseHash(latestReleaseHash ReleaseHash) error { if r.ClusterAddonDir == latestReleaseHash.ClusterAddonDir && r.ClusterAddonValues == latestReleaseHash.ClusterAddonValues && + r.ClusterClassDir == latestReleaseHash.ClusterClassDir && r.NodeImageDir == latestReleaseHash.NodeImageDir { return errors.New("no change in the cluster stack") } From 25df2810604ad708d6902312ee5825e8a02cbef3 Mon Sep 17 00:00:00 2001 From: Kurt Garloff Date: Wed, 6 May 2026 17:44:35 +0000 Subject: [PATCH 3/3] Update csctl-build-image to go-1.25. Being at it: - Updating alpine 3.23.0 -> 3.23.4 (latest patch update) - Lychee 0.15.1 -> 0.24.0 - golangci 2.7.2 -> 2.9.0 - trivy 0.68.2 -> 0.70.0 To get go-1.25, we had to switch from Bullseye (Debian 11) to Bookworm (12). That brought a few more changes: - skopeo 1.2.2 -> 1.9.3 - yamllint 1.37.1 -> 1.38.0 Signed-off-by: Kurt Garloff --- .builder-image-version.txt | 2 +- images/builder/Dockerfile | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.builder-image-version.txt b/.builder-image-version.txt index 71790396..53a75d67 100644 --- a/.builder-image-version.txt +++ b/.builder-image-version.txt @@ -1 +1 @@ -0.2.3 +0.2.6 diff --git a/images/builder/Dockerfile b/images/builder/Dockerfile index 229c2ee1..22df10a5 100644 --- a/images/builder/Dockerfile +++ b/images/builder/Dockerfile @@ -17,20 +17,20 @@ # If you make changes to this Dockerfile run `make builder-image-push`. # Install Lychee -FROM docker.io/library/alpine:3.23.0@sha256:51183f2cfa6320055da30872f211093f9ff1d3cf06f39a0bdb212314c5dc7375 as lychee +FROM docker.io/library/alpine:3.23.4@sha256:4d889c14e7d5a73929ab00be2ef8ff22437e7cbc545931e52554a7b00e123d8b as lychee # update: datasource=github-tags depName=lycheeverse/lychee versioning=semver -ENV LYCHEE_VERSION="v0.15.1" +ENV LYCHEE_VERSION="v0.24.2" # hadolint ignore=DL3018 RUN apk add --no-cache curl && \ - curl -L -o /tmp/lychee-${LYCHEE_VERSION}.tgz https://github.com/lycheeverse/lychee/releases/download/${LYCHEE_VERSION}/lychee-${LYCHEE_VERSION}-x86_64-unknown-linux-gnu.tar.gz && \ + curl -L -o /tmp/lychee-${LYCHEE_VERSION}.tgz https://github.com/lycheeverse/lychee/releases/download/lychee-${LYCHEE_VERSION}/lychee-x86_64-unknown-linux-gnu.tar.gz && \ tar -xz -C /tmp -f /tmp/lychee-${LYCHEE_VERSION}.tgz && \ - mv /tmp/lychee /usr/bin/lychee && \ + mv /tmp/lychee-x86_64-unknown-linux-gnu/lychee /usr/bin/lychee && \ rm -rf /tmp/linux-amd64 /tmp/lychee-${LYCHEE_VERSION}.tgz # Install Golang CI Lint -FROM docker.io/library/alpine:3.23.0@sha256:51183f2cfa6320055da30872f211093f9ff1d3cf06f39a0bdb212314c5dc7375 as golangci +FROM docker.io/library/alpine:3.23.4@sha256:4d889c14e7d5a73929ab00be2ef8ff22437e7cbc545931e52554a7b00e123d8b as golangci # update: datasource=github-tags depName=golangci/golangci-lint versioning=semver -ENV GOLANGCI_VERSION="v2.7.2" +ENV GOLANGCI_VERSION="v2.9.0" WORKDIR / # hadolint ignore=DL3018,DL4006 RUN apk add --no-cache curl && \ @@ -40,17 +40,17 @@ RUN apk add --no-cache curl && \ FROM docker.io/hadolint/hadolint:v2.14.0-alpine@sha256:7aba693c1442eb31c0b015c129697cb3b6cb7da589d85c7562f9deb435a6657c as hadolint # Install Trivy -FROM docker.io/aquasec/trivy:0.68.2@sha256:05d0126976bdedcd0782a0336f77832dbea1c81b9cc5e4b3a5ea5d2ec863aca7 as trivy +FROM docker.io/aquasec/trivy:0.70.0@sha256:be1190afcb28352bfddc4ddeb71470835d16462af68d310f9f4bca710961a41e as trivy ############################ # csctl Build Image Base # ############################ -FROM docker.io/library/golang:1.22-bullseye +FROM docker.io/library/golang:1.25-bookworm # update: datasource=repology depName=debian_11/skopeo versioning=loose -ENV SKOPEO_VERSION="1.2.2+dfsg1-1+b6" +ENV SKOPEO_VERSION="1.9.3+ds1-1+b10" # update: datasource=github-tags depName=adrienverge/yamllint versioning=semver -ENV YAMLLINT_VERSION="v1.37.1" +ENV YAMLLINT_VERSION="v1.38.0" # update: datasource=github-tags depName=opt-nc/yamlfixer versioning=semver ENV YAMLFIXER_VERSION="0.9.15" @@ -63,7 +63,7 @@ RUN apt-get update && \ protobuf-compiler libprotobuf-dev \ libsystemd-dev jq && \ rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \ - pip install --no-cache-dir \ + pip install --break-system-packages --no-cache-dir \ yamllint==${YAMLLINT_VERSION} \ yamlfixer-opt-nc==${YAMLFIXER_VERSION}