diff --git a/api/v1alpha1/clusterstack_types.go b/api/v1alpha1/clusterstack_types.go index 2cad61a94..e36c3cb5a 100644 --- a/api/v1alpha1/clusterstack_types.go +++ b/api/v1alpha1/clusterstack_types.go @@ -39,7 +39,7 @@ type ClusterStackSpec struct { // Channel specifies the release channel of the cluster stack. Defaults to 'stable'. // +kubebuilder:default:=stable - // +kubebuilder:validation:enum=stable;alpha;beta;rc + // +kubebuilder:validation:Enum=stable;custom Channel version.Channel `json:"channel,omitempty"` // Versions is a list of version of the cluster stack that should be available in the management cluster. diff --git a/config/crd/bases/clusterstack.x-k8s.io_clusterstacks.yaml b/config/crd/bases/clusterstack.x-k8s.io_clusterstacks.yaml index dd113a523..c608c50db 100644 --- a/config/crd/bases/clusterstack.x-k8s.io_clusterstacks.yaml +++ b/config/crd/bases/clusterstack.x-k8s.io_clusterstacks.yaml @@ -75,6 +75,9 @@ spec: default: stable description: Channel specifies the release channel of the cluster stack. Defaults to 'stable'. + enum: + - stable + - custom type: string kubernetesVersion: description: KubernetesVersion is the Kubernetes version in the format diff --git a/go.mod b/go.mod index 27556e4a7..cf560ee0d 100644 --- a/go.mod +++ b/go.mod @@ -96,7 +96,7 @@ require ( google.golang.org/protobuf v1.31.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 - gopkg.in/yaml.v3 v3.0.1 // indirect + gopkg.in/yaml.v3 v3.0.1 k8s.io/apiextensions-apiserver v0.27.2 k8s.io/apiserver v0.27.2 // indirect k8s.io/component-base v0.27.2 // indirect diff --git a/hack/kind-dev.sh b/hack/kind-dev.sh index 53b5bab20..0e79539ed 100755 --- a/hack/kind-dev.sh +++ b/hack/kind-dev.sh @@ -24,29 +24,26 @@ REPO_ROOT=$(git rev-parse --show-toplevel) cd "${REPO_ROOT}" || exit 1 # Creates a kind cluster with the ctlptl tool https://github.com/tilt-dev/ctlptl -ctlptl_kind-cluster-with-registry () { - -local CLUSTER_NAME=$1 -local CLUSTER_VERSION=$2 +ctlptl_kind-cluster-with-registry() { -cat </// + // For example: /tmp/downloads/cluster-stacks/docker-ferrol-1-26-v2/ downloadPath = filepath.Join(downloadPath, clusterStackSuffix, tag) - cs, err := clusterstack.NewFromString(tag) + cs, err := clusterstack.NewFromClusterStackReleaseProperties(tag) if err != nil { return Release{}, false, fmt.Errorf("failed to parse cluster stack release: %w", err) } @@ -92,6 +94,18 @@ func New(tag, downloadPath string) (Release, bool, error) { return rel, false, nil } +// ConvertFromClusterClassToClusterStackFormat converts `docker-ferrol-1-27-v0-sha.3960147` way to +// `docker-ferrol-1-27-v0-sha-3960147`. +func ConvertFromClusterClassToClusterStackFormat(input string) string { + parts := strings.Split(input, ".") + + if len(parts) == 2 { + return fmt.Sprintf("%s-%s", parts[0], parts[1]) + } + + return input +} + func ensureMetadata(downloadPath, metadataFileName string) (Metadata, error) { // Read the metadata.yaml file from the release. metadataPath := filepath.Join(downloadPath, metadataFileName) @@ -107,6 +121,25 @@ func ensureMetadata(downloadPath, metadataFileName string) (Metadata, error) { return Metadata{}, fmt.Errorf("failed to unmarshal metadata: %w", err) } + // Normalize the versions of metadata from v1-alpha.1 to v1-alpha-1 format. + metaClusterStackVersion, err := version.New(metadata.Versions.ClusterStack) + if err != nil { + return Metadata{}, fmt.Errorf("failed to parse ClusterStack version from metadata: %w", err) + } + metadata.Versions.ClusterStack = metaClusterStackVersion.String() + + metaClusterAddonVersion, err := version.New(metadata.Versions.Components.ClusterAddon) + if err != nil { + return Metadata{}, fmt.Errorf("failed to parse ClusterAddon version from metadata: %w", err) + } + metadata.Versions.Components.ClusterAddon = metaClusterAddonVersion.String() + + metaNodeImageVersion, err := version.New(metadata.Versions.Components.NodeImage) + if err != nil { + return Metadata{}, fmt.Errorf("failed to parse NodeImage version from metadata: %w", err) + } + metadata.Versions.Components.NodeImage = metaNodeImageVersion.String() + return metadata, nil } @@ -165,13 +198,15 @@ func (r *Release) Validate() error { // clusterAddonChartName returns the helm chart name for cluster addon. func (r *Release) clusterAddonChartName() string { - return fmt.Sprintf("%s-%s-%s-cluster-addon-%s", r.ClusterStack.Provider, r.ClusterStack.Name, r.ClusterStack.KubernetesVersion, r.Meta.Versions.Components.ClusterAddon) + clusterAddonVersion, _ := version.ParseVersionString(r.Meta.Versions.Components.ClusterAddon) + return fmt.Sprintf("%s-%s-%s-cluster-addon-%s", r.ClusterStack.Provider, r.ClusterStack.Name, r.ClusterStack.KubernetesVersion, clusterAddonVersion.StringWithDot()) } // ClusterAddonChartPath returns the helm chart name from the given path. func (r *Release) ClusterAddonChartPath() string { // we ignore the error here, since we already checked for the presence of the chart. - path, _ := r.helmChartNamePath(r.clusterAddonChartName()) + name := r.clusterAddonChartName() + path, _ := r.helmChartNamePath(name) return path } @@ -182,7 +217,7 @@ func (r *Release) ClusterAddonValuesPath() string { // clusterClassChartName returns the helm chart name for cluster class. func (r *Release) clusterClassChartName() string { - return fmt.Sprintf("%s-%s-%s-cluster-class-%s", r.ClusterStack.Provider, r.ClusterStack.Name, r.ClusterStack.KubernetesVersion, r.ClusterStack.Version.String()) + return fmt.Sprintf("%s-%s-%s-cluster-class-%s", r.ClusterStack.Provider, r.ClusterStack.Name, r.ClusterStack.KubernetesVersion, r.ClusterStack.Version.StringWithDot()) } // ClusterClassChartPath returns the absolute helm chart path for cluster class. diff --git a/pkg/version/channel.go b/pkg/version/channel.go index 3acc415cd..1dd33c6fa 100644 --- a/pkg/version/channel.go +++ b/pkg/version/channel.go @@ -23,18 +23,12 @@ type Channel string const ( // ChannelStable is the stable channel. ChannelStable = Channel("stable") - // ChannelAlpha is the alpha channel. - ChannelAlpha = Channel("alpha") - // ChannelBeta is the beta channel. - ChannelBeta = Channel("beta") - // ChannelRC is the rc channel. - ChannelRC = Channel("rc") + // ChannelCustom is the custom channel where git hash is used in the versioning. + ChannelCustom = Channel("custom") ) // IsValid returns true if the release channel is valid. func (c Channel) IsValid() bool { return c == ChannelStable || - c == ChannelAlpha || - c == ChannelBeta || - c == ChannelRC + c == ChannelCustom } diff --git a/pkg/version/version.go b/pkg/version/version.go index 69175d105..ec77f7cf8 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -34,27 +34,28 @@ import ( type Version struct { Major int Channel Channel - Patch int + Patch string } -// New returns a Version struct from a version string -// Sample allowed inputs: "v1-alpha-1", "v1", "v1-alpha-0" -// Sample disallowed inputs: "v1-alpha", "v1-alpha-1.0", "v1-alpha-1.0.0", "v1-alpha.", "v1.0-alpha.1". -func New(version string) (Version, error) { - var major, patch int - var err error +// ParseVersionString returns a Version struct from a version string like - +// "v1", "v1-alpha-1", "v1-beta-3", etc. +func ParseVersionString(version string) (Version, error) { + var ( + major int + patch string + err error + ) channel := ChannelStable - re := regexp.MustCompile(`^v\d+(-\b\w+\b-\d+)?$`) - match := re.FindStringSubmatch(version) - - if len(match) == 0 { + re := regexp.MustCompile(`^v\d+(-\b\w+\b\-\w+)?$`) + match := re.MatchString(version) + if !match { return Version{}, fmt.Errorf("invalid version string %s", version) } // match[0] is the entire string e.g "v1-alpha-1" or "v1" // split match[0] with "-" as the delimiter - ver := strings.Split(match[0], "-") + ver := strings.Split(version, "-") // ver[0] is the major version // trim the "v" prefix and then convert to int @@ -67,9 +68,57 @@ func New(version string) (Version, error) { // ver[2] is the patch if len(ver) == 3 { channel = Channel(ver[1]) - if patch, err = strconv.Atoi(ver[2]); err != nil { - return Version{}, fmt.Errorf("invalid patch value in version %s", ver[2]) + patch = ver[2] + } + clusterStackVersion := Version{ + Major: major, + Channel: channel, + Patch: patch, + } + if err := clusterStackVersion.Validate(); err != nil { + return Version{}, err + } + + return clusterStackVersion, nil +} + +// New returns a Version struct from a version string +// Sample allowed inputs: "v1-alpha.1", "v1", "v1-alpha.0" +// Sample disallowed inputs: "v1-alpha", "v1-alpha-1.0", "v1-alpha-1.0.0", "v1-alpha.", "v1.0-alpha.1". +func New(version string) (Version, error) { + var ( + major int + patch string + err error + ) + channel := ChannelStable + + re := regexp.MustCompile(`^v\d+(-\b\w+\b\.\w+)?$`) + match := re.MatchString(version) + if !match { + return Version{}, fmt.Errorf("invalid version string %s", version) + } + + // match[0] is the entire string e.g "v1-alpha.1" or "v1" + // split match[0] with "-" as the delimiter + ver := strings.Split(version, "-") + + // ver[0] is the major version + // trim the "v" prefix and then convert to int + if major, err = strconv.Atoi(strings.TrimPrefix(ver[0], "v")); err != nil { + return Version{}, fmt.Errorf("invalid major version %s", ver[0]) + } + + // If the length of ver is 2, then the version string is of the form "v1-alpha.1", and split it - + // ver[0] is the channel + // ver[1] is the patch + if len(ver) == 2 { + splittedChannelPatch := strings.Split(ver[1], ".") + if len(splittedChannelPatch) != 2 { + return Version{}, fmt.Errorf("invalid version string %s", version) } + channel = Channel(splittedChannelPatch[0]) + patch = splittedChannelPatch[1] } clusterStackVersion := Version{ @@ -86,13 +135,15 @@ func New(version string) (Version, error) { // FromReleaseTag returns a Version struct from a release tag string. func FromReleaseTag(releaseTag string) (Version, error) { v := strings.Split(releaseTag, "-") - if len(v) != 5 && len(v) != 6 { + if len(v) != 5 && len(v) != 7 { return Version{}, fmt.Errorf("invalid release tag %s", releaseTag) } + // for docker-ferrol-1-26-v1 type tag, v[4] is the version if len(v) == 5 { - return New(v[4]) + return ParseVersionString(v[4]) } - return New(fmt.Sprintf("%s-%s", v[4], v[5])) + // for docker-ferrol-1-26-v1-alpha-0 type tag, v[4] is the version and v[5] is the release channel + patch version + return ParseVersionString(fmt.Sprintf("%s-%s-%s", v[4], v[5], v[6])) } // Validate validates the version. @@ -100,15 +151,32 @@ func (csv *Version) Validate() error { if csv.Major < 0 { return fmt.Errorf("major version should be a non-negative integer") } - if !csv.Channel.IsValid() { - return fmt.Errorf("invalid channel: %s", csv.Channel) - } - if csv.Patch < 0 { - return fmt.Errorf("patch version should be a non-negative integer") + + if csv.Channel != ChannelStable { + // Check if the patch is a valid integer + if isInteger(csv.Patch) { + // If it's an integer, check if it's greater than 0 + patchInt, _ := strconv.Atoi(csv.Patch) + if patchInt < 0 { + return fmt.Errorf("patch version should be a non-negative integer") + } + } + + // If it's alpha numeric, check if it's empty + if csv.Patch == "" { + return fmt.Errorf("patch can't empty") + } } + return nil } +// isInteger checks if the given string is a valid integer. +func isInteger(s string) bool { + _, err := strconv.Atoi(s) + return err == nil +} + // Compare compares two Version structs // Returns 1 if csv is greater than input // Returns -1 if csv is less than input @@ -138,9 +206,23 @@ func (csv Version) Compare(input Version) (int, error) { return 0, nil } +// String converts a Version struct to a string representation. +// If the channel is stable, it returns the version in the format "vMajor". +// Otherwise, it returns the version in the format "vMajor-Channel-Patch". func (csv Version) String() string { if csv.Channel == ChannelStable { return fmt.Sprintf("v%d", csv.Major) } - return fmt.Sprintf("v%d-%s-%d", csv.Major, csv.Channel, csv.Patch) + return fmt.Sprintf("v%d-%s-%s", csv.Major, csv.Channel, csv.Patch) +} + +// StringWithDot converts a Version struct to a string representation. +// If the channel is stable, it returns the version in the format "vMajor". +// Otherwise, it returns the version in the format "vMajor-Channel.Patch", +// similar to String but with a dot separating channel and patch. +func (csv Version) StringWithDot() string { + if csv.Channel == ChannelStable { + return fmt.Sprintf("v%d", csv.Major) + } + return fmt.Sprintf("v%d-%s.%s", csv.Major, csv.Channel, csv.Patch) }