ecm-distro-tools/release/release.go

1052 lines
34 KiB
Go

package release
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"path/filepath"
"regexp"
"sort"
"strings"
"text/template"
"time"
"unicode"
"github.com/google/go-github/v39/github"
httpecm "github.com/rancher/ecm-distro-tools/http"
"github.com/rancher/ecm-distro-tools/repository"
"github.com/sirupsen/logrus"
"golang.org/x/mod/modfile"
"golang.org/x/mod/semver"
"gopkg.in/yaml.v3"
)
const (
k3sRepo = "k3s"
rke2Repo = "rke2"
uiRepo = "ui"
dashboardRepo = "dashboard"
alternateVersion = "1.23"
rke2ChartsVersionsFile = "chart_versions.yaml"
defaultTimeout = 30 * time.Second
)
type charts struct {
Charts []chart `yaml:"charts"`
}
type chart struct {
Version string `yaml:"version"`
Filename string `yaml:"filename"`
Bootstrap bool `yaml:"bootstrap"`
}
type changeLogData struct {
PrevMilestone string
Content []repository.ChangeLog
}
type rke2ReleaseNoteData struct {
Milestone string
K8sVersion string
MajorMinor string
EtcdVersion string
ContainerdVersion string
RuncVersion string
MetricsServerVersion string
CoreDNSVersion string
ChangeLogVersion string
IngressNginxVersion string
HelmControllerVersion string
FlannelVersion string
CanalCalicoVersion string
CanalCalicoURL string
CalicoVersion string
CalicoURL string
CiliumVersion string
MultusVersion string
ChangeLogData changeLogData
CiliumChartVersion string
CanalChartVersion string
CalicoChartVersion string
CalicoCRDChartVersion string
CoreDNSChartVersion string
IngressNginxChartVersion string
MetricsServerChartVersion string
VsphereCSIChartVersion string
VsphereCPIChartVersion string
HarvesterCloudProviderChartVersion string
HarvesterCSIDriverChartVersion string
SnapshotControllerChartVersion string
SnapshotControllerCRDChartVersion string
SnapshotValidationWebhookChartVersion string
}
type k3sReleaseNoteData struct {
Milestone string
K8sVersion string
MajorMinor string
ChangeLogSince string
ChangeLogVersion string
KineVersion string
SQLiteVersion string
SQLiteVersionReplaced string
EtcdVersion string
ContainerdVersion string
RuncVersion string
FlannelVersion string
MetricsServerVersion string
TraefikVersion string
CoreDNSVersion string
HelmControllerVersion string
LocalPathProvisionerVersion string
ChangeLogData changeLogData
}
type uiReleaseNoteData struct {
Milestone string
MajorMinor string
ChangeLogVersion string
ChangeLogData changeLogData
}
type dashboardReleaseNoteData struct {
Milestone string
MajorMinor string
ChangeLogVersion string
ChangeLogData changeLogData
}
func majMin(v string) (string, error) {
majMin := semver.MajorMinor(v)
if majMin == "" {
return "", errors.New("version is not valid")
}
return majMin, nil
}
func trimPeriods(v string) string {
return strings.ReplaceAll(v, ".", "")
}
// capitalize returns a new string whose first letter is capitalized.
func capitalize(s string) string {
if runes := []rune(s); len(runes) > 0 {
for i, r := range runes {
if unicode.IsLetter(r) {
runes[i] = unicode.ToUpper(r)
s = string(runes)
break
}
}
}
return s
}
// GenReleaseNotes genereates release notes based on the given milestone,
// previous milestone, and repository.
func GenReleaseNotes(ctx context.Context, owner, repo, milestone, prevMilestone string, client *github.Client) (*bytes.Buffer, error) {
funcMap := template.FuncMap{
"majMin": majMin,
"trimPeriods": trimPeriods,
"split": strings.Split,
"capitalize": capitalize,
}
const templateName = "release-notes"
tmpl := template.New(templateName).Funcs(funcMap)
tmpl = template.Must(tmpl.Parse(changelogTemplate))
content, err := repository.RetrieveChangeLogContents(ctx, client, owner, repo, prevMilestone, milestone)
if err != nil {
return nil, err
}
// account for processing against an rc
milestoneNoRC := milestone
idx := strings.Index(milestone, "-rc")
if idx != -1 {
tmpMilestone := []rune(milestone)
tmpMilestone = append(tmpMilestone[0:idx], tmpMilestone[idx+4:]...)
milestoneNoRC = string(tmpMilestone)
}
k8sVersion := strings.Split(milestoneNoRC, "+")[0]
markdownVersion := strings.ReplaceAll(k8sVersion, ".", "")
tmp := strings.Split(strings.ReplaceAll(k8sVersion, "v", ""), ".")
var majorMinor string
if len(tmp) > 1 {
majorMinor = tmp[0] + "." + tmp[1]
} else {
// for master branch
majorMinor = tmp[0]
}
changeLogSince := strings.ReplaceAll(strings.Split(prevMilestone, "+")[0], ".", "")
sqliteVersionK3S := goModLibVersion("go-sqlite3", repo, milestone)
sqliteVersionBinding := sqliteVersionBinding(sqliteVersionK3S)
helmControllerVersion := goModLibVersion("helm-controller", repo, milestone)
coreDNSVersion := imageTagVersion("coredns", repo, milestone)
cgData := changeLogData{
PrevMilestone: prevMilestone,
Content: content,
}
if repo == k3sRepo {
return genK3SReleaseNotes(
tmpl,
milestone,
k3sReleaseNoteData{
Milestone: milestoneNoRC,
MajorMinor: majorMinor,
K8sVersion: k8sVersion,
ChangeLogVersion: markdownVersion,
ChangeLogSince: changeLogSince,
SQLiteVersion: sqliteVersionBinding,
SQLiteVersionReplaced: strings.ReplaceAll(sqliteVersionBinding, ".", "_"),
HelmControllerVersion: helmControllerVersion,
ChangeLogData: cgData,
CoreDNSVersion: coreDNSVersion,
},
)
}
if repo == rke2Repo {
return genRKE2ReleaseNotes(
tmpl,
milestone,
rke2ReleaseNoteData{
MajorMinor: majorMinor,
Milestone: milestoneNoRC,
ChangeLogVersion: markdownVersion,
K8sVersion: k8sVersion,
HelmControllerVersion: helmControllerVersion,
CoreDNSVersion: coreDNSVersion,
ChangeLogData: cgData,
},
)
}
if repo == uiRepo {
return genUIReleaseNotes(
tmpl,
milestone,
uiReleaseNoteData{
MajorMinor: majorMinor,
Milestone: milestoneNoRC,
ChangeLogVersion: markdownVersion,
ChangeLogData: cgData,
},
)
}
if repo == dashboardRepo {
return genDashboardReleaseNotes(
tmpl,
milestone,
dashboardReleaseNoteData{
MajorMinor: majorMinor,
Milestone: milestoneNoRC,
ChangeLogVersion: markdownVersion,
ChangeLogData: cgData,
},
)
}
return nil, errors.New("invalid repo: it must be k3s, rke2, ui or dashboard")
}
func genK3SReleaseNotes(tmpl *template.Template, milestone string, rd k3sReleaseNoteData) (*bytes.Buffer, error) {
tmpl = template.Must(tmpl.Parse(k3sReleaseNoteTemplate))
var runcVersion string
var containerdVersion string
if semver.Compare(rd.K8sVersion, "v1.24.0") == 1 && semver.Compare(rd.K8sVersion, "v1.26.5") == -1 {
containerdVersion = buildScriptVersion("VERSION_CONTAINERD", k3sRepo, milestone)
} else {
containerdVersion = goModLibVersion("containerd/containerd", k3sRepo, milestone)
}
if rd.MajorMinor == alternateVersion {
runcVersion = buildScriptVersion("VERSION_RUNC", k3sRepo, milestone)
} else {
runcVersion = goModLibVersion("runc", k3sRepo, milestone)
}
rd.KineVersion = goModLibVersion("kine", k3sRepo, milestone)
rd.EtcdVersion = goModLibVersion("etcd/api/v3", k3sRepo, milestone)
rd.ContainerdVersion = containerdVersion
rd.RuncVersion = runcVersion
rd.FlannelVersion = goModLibVersion("flannel", k3sRepo, milestone)
rd.MetricsServerVersion = imageTagVersion("metrics-server", k3sRepo, milestone)
rd.TraefikVersion = imageTagVersion("traefik", k3sRepo, milestone)
rd.LocalPathProvisionerVersion = imageTagVersion("local-path-provisioner", k3sRepo, milestone)
b := bytes.NewBuffer(nil)
if err := tmpl.ExecuteTemplate(b, k3sRepo, rd); err != nil {
return nil, err
}
return b, nil
}
func genRKE2ReleaseNotes(tmpl *template.Template, milestone string, rd rke2ReleaseNoteData) (*bytes.Buffer, error) {
tmpl = template.Must(tmpl.Parse(rke2ReleaseNoteTemplate))
var containerdVersion string
if rd.MajorMinor == alternateVersion {
containerdVersion = goModLibVersion("containerd/containerd", rke2Repo, milestone)
} else {
containerdVersion = dockerfileVersion("hardened-containerd", rke2Repo, milestone)
}
rd.EtcdVersion = buildScriptVersion("ETCD_VERSION", rke2Repo, milestone)
rd.RuncVersion = dockerfileVersion("hardened-runc", rke2Repo, milestone)
rd.CanalCalicoVersion = imageTagVersion("hardened-calico", rke2Repo, milestone)
rd.CanalCalicoURL = createCalicoURL(rd.CanalCalicoVersion)
rd.CiliumVersion = imageTagVersion("cilium-cilium", rke2Repo, milestone)
rd.ContainerdVersion = containerdVersion
rd.MetricsServerVersion = imageTagVersion("metrics-server", rke2Repo, milestone)
rd.IngressNginxVersion = imageTagVersion("nginx-ingress-controller", rke2Repo, milestone)
rd.FlannelVersion = imageTagVersion("flannel", rke2Repo, milestone)
rd.MultusVersion = imageTagVersion("multus-cni", rke2Repo, milestone)
rd.CalicoVersion = imageTagVersion("calico-node", rke2Repo, milestone)
rd.CalicoURL = createCalicoURL(rd.CalicoVersion)
// get charts versions
chartsData, err := rke2ChartsVersion(milestone)
if err != nil {
return nil, err
}
rd.CiliumChartVersion = chartsData["rke2-cilium.yaml"].Version
rd.CanalChartVersion = chartsData["rke2-canal.yaml"].Version
rd.CalicoChartVersion = chartsData["rke2-calico.yaml"].Version
rd.CalicoCRDChartVersion = chartsData["rke2-calico-crd.yaml"].Version
rd.CoreDNSChartVersion = chartsData["rke2-coredns.yaml"].Version
rd.IngressNginxChartVersion = chartsData["rke2-ingress-nginx.yaml"].Version
rd.MetricsServerChartVersion = chartsData["rke2-metrics-server.yaml"].Version
rd.VsphereCSIChartVersion = chartsData["rancher-vsphere-csi.yaml"].Version
rd.VsphereCPIChartVersion = chartsData["rancher-vsphere-cpi.yaml"].Version
rd.HarvesterCloudProviderChartVersion = chartsData["harvester-cloud-provider.yaml"].Version
rd.HarvesterCSIDriverChartVersion = chartsData["harvester-csi-driver.yaml"].Version
rd.SnapshotControllerChartVersion = chartsData["rke2-snapshot-controller.yaml"].Version
rd.SnapshotControllerCRDChartVersion = chartsData["rke2-snapshot-controller-crd.yaml"].Version
rd.SnapshotValidationWebhookChartVersion = chartsData["rke2-snapshot-validation-webhook.yaml"].Version
b := bytes.NewBuffer(nil)
if err := tmpl.ExecuteTemplate(b, rke2Repo, rd); err != nil {
return nil, err
}
return b, nil
}
func genUIReleaseNotes(tmpl *template.Template, _ string, rd uiReleaseNoteData) (*bytes.Buffer, error) {
uiTemplate := fmt.Sprintf(dashboardReleaseNoteTemplate, "ui")
tmpl = template.Must(tmpl.Parse(uiTemplate))
b := bytes.NewBuffer(nil)
if err := tmpl.ExecuteTemplate(b, uiRepo, rd); err != nil {
return nil, err
}
return b, nil
}
func genDashboardReleaseNotes(tmpl *template.Template, _ string, rd dashboardReleaseNoteData) (*bytes.Buffer, error) {
dashboardTemplate := fmt.Sprintf(dashboardReleaseNoteTemplate, "dashboard")
tmpl = template.Must(tmpl.Parse(dashboardTemplate))
b := bytes.NewBuffer(nil)
if err := tmpl.ExecuteTemplate(b, dashboardRepo, rd); err != nil {
return nil, err
}
return b, nil
}
// CheckUpstreamRelease takes the given org, repo, and tags and checks
// for the tags' existence.
func CheckUpstreamRelease(ctx context.Context, client *github.Client, org, repo string, tags []string) (map[string]bool, error) {
releases := make(map[string]bool, len(tags))
for _, tag := range tags {
_, _, err := client.Repositories.GetReleaseByTag(ctx, org, repo, tag)
if err != nil {
switch err := err.(type) {
case *github.ErrorResponse:
if err.Response.StatusCode != http.StatusNotFound {
return nil, err
}
releases[tag] = false
continue
default:
return nil, err
}
}
releases[tag] = true
}
return releases, nil
}
func KubernetesGoVersion(ctx context.Context, client *github.Client, version string) (string, error) {
var githubError *github.ErrorResponse
file, _, _, err := client.Repositories.GetContents(ctx, "kubernetes", "kubernetes", ".go-version", &github.RepositoryContentGetOptions{
Ref: version,
})
if err != nil {
if errors.As(err, &githubError) {
if githubError.Response.StatusCode == http.StatusNotFound {
return "", err
}
}
return "", err
}
goVersion, err := file.GetContent()
if err != nil {
return "", err
}
return strings.Trim(goVersion, "\n"), nil
}
// VerifyAssets checks the number of assets for the
// given release and indicates if the expected number has
// been met.
func VerifyAssets(ctx context.Context, client *github.Client, owner, repo string, tags []string) (map[string]bool, error) {
if len(tags) == 0 {
return nil, errors.New("no tags provided")
}
releases := make(map[string]bool, len(tags))
const (
rke2Assets = 50
k3sAssets = 23
rke2Packaging = 23
)
for _, tag := range tags {
if tag == "" {
continue
}
release, _, err := client.Repositories.GetReleaseByTag(ctx, owner, repo, tag)
if err != nil {
switch err := err.(type) {
case *github.ErrorResponse:
if err.Response.StatusCode != http.StatusNotFound {
return nil, err
}
releases[tag] = false
continue
default:
return nil, err
}
}
if repo == rke2Repo && len(release.Assets) == rke2Assets {
releases[tag] = true
}
if repo == k3sRepo && len(release.Assets) == k3sAssets {
releases[tag] = true
}
if repo == "rke2-packing" && len(release.Assets) == rke2Packaging {
releases[tag] = true
}
}
return releases, nil
}
// ListAssets gets all assets associated with the given release.
func ListAssets(ctx context.Context, client *github.Client, owner, repo, tag string) ([]*github.ReleaseAsset, error) {
if tag == "" {
return nil, errors.New("invalid tag provided")
}
release, _, err := client.Repositories.GetReleaseByTag(ctx, owner, repo, tag)
if err != nil {
switch err := err.(type) {
case *github.ErrorResponse:
if err.Response.StatusCode != http.StatusNotFound {
return nil, err
}
default:
return nil, err
}
}
return release.Assets, nil
}
// DeleteAssetsByRelease deletes all release assets for the given release tag.
func DeleteAssetsByRelease(ctx context.Context, client *github.Client, owner, repo, tag string) error {
if tag == "" {
return errors.New("invalid tag provided")
}
release, _, err := client.Repositories.GetReleaseByTag(ctx, owner, repo, tag)
if err != nil {
switch err := err.(type) {
case *github.ErrorResponse:
if err.Response.StatusCode != http.StatusNotFound {
return err
}
default:
return err
}
}
for _, asset := range release.Assets {
if _, err := client.Repositories.DeleteReleaseAsset(ctx, owner, repo, asset.GetID()); err != nil {
return err
}
}
return nil
}
// DeleteAssetByID deletes the release asset associated with the given ID.
func DeleteAssetByID(ctx context.Context, client *github.Client, owner, repo, tag string, id int64) error {
if tag == "" {
return errors.New("invalid tag provided")
}
if _, err := client.Repositories.DeleteReleaseAsset(ctx, owner, repo, id); err != nil {
return err
}
return nil
}
func goModLibVersion(libraryName, repo, branchVersion string) string {
repoName := "k3s-io/k3s"
if repo == rke2Repo {
repoName = "rancher/rke2"
}
goModURL := "https://raw.githubusercontent.com/" + repoName + "/" + branchVersion + "/go.mod"
resp, err := http.Get(goModURL)
if err != nil {
logrus.Debugf("failed to fetch url %s: %v", goModURL, err)
return ""
}
if resp.StatusCode != http.StatusOK {
logrus.Debugf("status error: %v when fetching %s", resp.StatusCode, goModURL)
return ""
}
b, err := io.ReadAll(resp.Body)
if err != nil {
logrus.Debugf("read body error: %v", err)
return ""
}
modFile, err := modfile.Parse("go.mod", b, nil)
if err != nil {
logrus.Debugf("failed to parse go.mod file: %v", err)
return ""
}
// use replace section if found
for _, replace := range modFile.Replace {
if strings.Contains(replace.Old.Path, libraryName) {
return replace.New.Version
}
}
// if replace not found search in require
for _, require := range modFile.Require {
if strings.Contains(require.Mod.Path, libraryName) {
return require.Mod.Version
}
}
logrus.Debugf("library %s not found", libraryName)
return ""
}
func buildScriptVersion(varName, repo, branchVersion string) string {
repoName := "k3s-io/k3s"
if repo == rke2Repo {
repoName = "rancher/rke2"
}
buildScriptURL := "https://raw.githubusercontent.com/" + repoName + "/" + branchVersion + "/scripts/version.sh"
const regex = `(?P<version>v[\d\.]+(-k3s.\w*)?)`
submatch := findInURL(buildScriptURL, regex, varName, true)
if len(submatch) > 1 {
return submatch[1]
}
return ""
}
func dockerfileVersion(chartName, repo, branchVersion string) string {
if strings.Contains(repo, "k3s") {
return ""
}
const (
repoName = "rancher/rke2"
regex = `FROM\s+[\w-]+/[\w-]+:(.*?)(-build.*)?\s`
)
dockerfileURL := "https://raw.githubusercontent.com/" + repoName + "/" + branchVersion + "/Dockerfile"
submatch := findInURL(dockerfileURL, regex, chartName, true)
if len(submatch) > 1 {
return submatch[1]
}
return ""
}
func imageTagVersion(ImageName, repo, branchVersion string) string {
repoName := "k3s-io/k3s"
imageListURL := "https://raw.githubusercontent.com/" + repoName + "/" + branchVersion + "/scripts/airgap/image-list.txt"
if repo == rke2Repo {
repoName = "rancher/rke2"
imageListURL = "https://raw.githubusercontent.com/" + repoName + "/" + branchVersion + "/scripts/build-images"
}
const regex = `:(.*)(-build.*)?`
submatch := findInURL(imageListURL, regex, ImageName, true)
if len(submatch) > 1 {
if strings.Contains(submatch[1], "-build") {
versionSplit := strings.Split(submatch[1], "-")
return versionSplit[0]
}
return submatch[1]
}
return ""
}
func sqliteVersionBinding(sqliteVersion string) string {
sqliteBindingURL := "https://raw.githubusercontent.com/mattn/go-sqlite3/" + sqliteVersion + "/sqlite3-binding.h"
const (
regex = `\"(.*)\"`
word = "SQLITE_VERSION"
)
submatch := findInURL(sqliteBindingURL, regex, word, true)
if len(submatch) > 1 {
return submatch[1]
}
return ""
}
func createCalicoURL(calicoVersion string) string {
const (
regex = `\"(.*)\"`
notFound = "Page Not Found"
)
versionRegex := regexp.MustCompile(`^v(\d+\.\d+)(?:\.\d+)?$`)
formattedVersion := calicoVersion
// Check if the version matches the pattern
if versionRegex.MatchString(calicoVersion) {
matches := versionRegex.FindStringSubmatch(calicoVersion)
if len(matches) == 2 {
formattedVersion = "v" + matches[1]
}
}
calicoArchiveURL := "https://projectcalico.docs.tigera.io/archive/" + formattedVersion + "/release-notes/#" + strings.Trim(calicoVersion, "")
// check if doesn't exists content for archive url
submatch := findInURL(calicoArchiveURL, regex, notFound, false)
if len(submatch) > 1 {
return "https://docs.tigera.io/calico/latest/release-notes/#" + formattedVersion
}
return calicoArchiveURL
}
// findInURL will get and scan a url to find a slice submatch for all the words that matches a regex
// if the regex is empty then it will return the lines in a file that matches the str
func findInURL(url, regex, str string, checkStatusCode bool) []string {
var submatch []string
client := httpecm.NewClient(defaultTimeout)
resp, err := client.Get(url)
if err != nil {
logrus.Debugf("failed to fetch url %s: %v", url, err)
return nil
}
defer resp.Body.Close()
if checkStatusCode && resp.StatusCode != http.StatusOK {
logrus.Debugf("status error: %v when fetching %s", resp.StatusCode, url)
return nil
}
b, err := io.ReadAll(resp.Body)
if err != nil {
logrus.Debugf("read body error: %v", err)
return nil
}
scanner := bufio.NewScanner(strings.NewReader(string(b)))
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, str) {
if regex == "" {
submatch = append(submatch, line)
} else {
re := regexp.MustCompile(regex)
submatch = re.FindStringSubmatch(line)
if len(submatch) > 1 {
return submatch
}
}
}
}
return submatch
}
// LatestRC will get the latest rc created for the k8s version in either rke2 or k3s
func LatestRC(ctx context.Context, owner, repo, k8sVersion, projectSuffix string, client *github.Client) (*string, error) {
var rcs []*github.RepositoryRelease
allReleases, _, err := client.Repositories.ListReleases(ctx, owner, repo, &github.ListOptions{
Page: 0,
PerPage: 40,
})
if err != nil {
return nil, err
}
for _, release := range allReleases {
if strings.Contains(*release.TagName, k8sVersion+"-rc") && strings.Contains(*release.TagName, projectSuffix) {
rcs = append(rcs, release)
}
}
return latestRelease(rcs), nil
}
func LatestPreRelease(ctx context.Context, client *github.Client, owner, repo, version, preReleaseSuffix string) (*string, error) {
var versions []*github.RepositoryRelease
allReleases, _, err := client.Repositories.ListReleases(ctx, owner, repo, &github.ListOptions{})
if err != nil {
return nil, err
}
for _, release := range allReleases {
if strings.Contains(*release.TagName, version+"-"+preReleaseSuffix) {
versions = append(versions, release)
}
}
return latestRelease(versions), nil
}
// StatsMonthly
type StatsMonthly struct {
Count int
Captains []string
Tags []string
}
// RelStats
type RelStats struct {
Count int
Monthly map[time.Month]StatsMonthly
}
// StatsData
type StatsData struct {
Total int64 `json:"total"`
Data map[int]RelStats `json:"data"`
Captains map[string]int `json:"captains"`
}
// dedup creates and returns a new slices based on the given slice
// but with duplicate entries removed.
func dedup(slice []string) []string {
seen := make(map[string]struct{})
result := []string{}
for _, val := range slice {
if _, ok := seen[val]; !ok {
seen[val] = struct{}{}
result = append(result, val)
}
}
return result
}
// Stats collects and processes information regarding a set of releases for the given repo
// over the given period of time.
func Stats(ctx context.Context, client *github.Client, startDate, endDate time.Time, owner, repo string) (*StatsData, error) {
if endDate.Before(startDate) {
return nil, errors.New("end date before start date")
}
sd := StatsData{
Data: make(map[int]RelStats),
Captains: make(map[string]int),
}
lo := github.ListOptions{
PerPage: 100,
}
for {
releases, resp, err := client.Repositories.ListReleases(ctx, owner, repo, &lo)
if err != nil {
return nil, err
}
for _, release := range releases {
releaseDate := release.GetCreatedAt().Time
if releaseDate.After(startDate) && (releaseDate.Before(endDate) || releaseDate.Equal(endDate)) {
sd.Total++
if _, ok := sd.Data[int(release.CreatedAt.Year())]; !ok {
sd.Data[int(release.CreatedAt.Year())] = RelStats{
Count: 1,
Monthly: map[time.Month]StatsMonthly{
release.CreatedAt.Month(): {
Count: 1,
Captains: []string{
*release.Author.Login,
},
Tags: []string{
*release.Name,
},
},
},
}
continue
}
rs := sd.Data[int(release.CreatedAt.Year())]
rs.Count++
mon := rs.Monthly[release.CreatedAt.Month()]
mon.Count++
mon.Captains = append(mon.Captains, *release.Author.Login)
mon.Tags = append(mon.Tags, *release.Name)
rs.Monthly[release.CreatedAt.Month()] = mon
sd.Data[int(release.CreatedAt.Year())] = rs
if release.Author.Login != nil {
if _, ok := sd.Captains[*release.Author.Login]; !ok {
sd.Captains[*release.Author.Login]++
continue
}
sd.Captains[*release.Author.Login]++
}
}
}
if resp.NextPage == 0 {
break
}
lo.Page = resp.NextPage
}
for year := range sd.Data {
months := make([]int, 0, len(sd.Data[year].Monthly))
for k := range sd.Data[year].Monthly {
months = append(months, int(k))
}
sort.Ints(months)
for _, m := range months {
mon := time.Month(m)
tmp := sd.Data[year].Monthly[mon]
tmp.Captains = dedup(tmp.Captains)
sd.Data[year].Monthly[mon] = tmp
}
}
return &sd, nil
}
func latestRelease(versions []*github.RepositoryRelease) *string {
sort.Slice(versions, func(i, j int) bool {
return versions[i].PublishedAt.Before(versions[j].PublishedAt.Time)
})
if len(versions) == 0 {
return nil
}
return versions[len(versions)-1].TagName
}
// rke2ChartVersion will return the version of the rke2 chart from the chart versions file
func rke2ChartsVersion(branchVersion string) (map[string]chart, error) {
chartVersionsURL := "https://raw.githubusercontent.com/rancher/rke2/" + branchVersion + "/charts/" + rke2ChartsVersionsFile
client := httpecm.NewClient(defaultTimeout)
resp, err := client.Get(chartVersionsURL)
if err != nil {
logrus.Debugf("failed to fetch url %s: %v", chartVersionsURL, err)
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
logrus.Debugf("status error: %v when fetching %s", resp.StatusCode, err)
return nil, err
}
var c charts
if err := yaml.NewDecoder(resp.Body).Decode(&c); err != nil {
return nil, err
}
chartsData := make(map[string]chart, len(c.Charts))
for _, chart := range c.Charts {
chartsData[filepath.Base(chart.Filename)] = chart
}
return chartsData, nil
}
var changelogTemplate = `
{{- define "changelog" -}}
## Changes since {{.ChangeLogData.PrevMilestone}}:
{{range .ChangeLogData.Content}}
* {{ capitalize .Title }} [(#{{.Number}})]({{.URL}})
{{- $lines := split .Note "\n"}}
{{- range $i, $line := $lines}}
{{- if ne $line "" }}
* {{ capitalize $line }}
{{- end}}
{{- end}}
{{- end}}
{{- end}}`
const rke2ReleaseNoteTemplate = `
{{- define "rke2" -}}
<!-- {{.Milestone}} -->
This release updates Kubernetes to {{.K8sVersion}}.
**Important Note**
If your server (control-plane) nodes were not started with the ` + "`--token`" + ` CLI flag or config file key, a randomized token was generated during initial cluster startup. This key is used both for joining new nodes to the cluster, and for encrypting cluster bootstrap data within the datastore. Ensure that you retain a copy of this token, as is required when restoring from backup.
You may retrieve the token value from any server already joined to the cluster:
` + "```bash" + `
cat /var/lib/rancher/rke2/server/token
` + "```" + `
{{ template "changelog" . }}
## Charts Versions
| Component | Version |
| --- | --- |
| rke2-cilium | [{{.CiliumChartVersion}}](https://github.com/rancher/rke2-charts/raw/main/assets/rke2-cilium/rke2-cilium-{{.CiliumChartVersion}}.tgz) |
| rke2-canal | [{{.CanalChartVersion}}](https://github.com/rancher/rke2-charts/raw/main/assets/rke2-canal/rke2-canal-{{.CanalChartVersion}}.tgz) |
| rke2-calico | [{{.CalicoChartVersion}}](https://github.com/rancher/rke2-charts/raw/main/assets/rke2-calico/rke2-calico-{{.CalicoChartVersion}}.tgz) |
| rke2-calico-crd | [{{.CalicoCRDChartVersion}}](https://github.com/rancher/rke2-charts/raw/main/assets/rke2-calico/rke2-calico-crd-{{.CalicoCRDChartVersion}}.tgz) |
| rke2-coredns | [{{.CoreDNSChartVersion}}](https://github.com/rancher/rke2-charts/raw/main/assets/rke2-coredns/rke2-coredns-{{.CoreDNSChartVersion}}.tgz) |
| rke2-ingress-nginx | [{{.IngressNginxChartVersion}}](https://github.com/rancher/rke2-charts/raw/main/assets/rke2-ingress-nginx/rke2-ingress-nginx-{{.IngressNginxChartVersion}}.tgz) |
| rke2-metrics-server | [{{.MetricsServerChartVersion}}](https://github.com/rancher/rke2-charts/raw/main/assets/rke2-metrics-server/rke2-metrics-server-{{.MetricsServerChartVersion}}.tgz) |
| rancher-vsphere-csi | [{{.VsphereCSIChartVersion}}](https://github.com/rancher/rke2-charts/raw/main/assets/rancher-vsphere-csi/rancher-vsphere-csi-{{.VsphereCSIChartVersion}}.tgz) |
| rancher-vsphere-cpi | [{{.VsphereCPIChartVersion}}](https://github.com/rancher/rke2-charts/raw/main/assets/rancher-vsphere-cpi/rancher-vsphere-cpi-{{.VsphereCPIChartVersion}}.tgz) |
| harvester-cloud-provider | [{{.HarvesterCloudProviderChartVersion}}](https://github.com/rancher/rke2-charts/raw/main/assets/harvester-cloud-provider/harvester-cloud-provider-{{.HarvesterCloudProviderChartVersion}}.tgz) |
| harvester-csi-driver | [{{.HarvesterCSIDriverChartVersion}}](https://github.com/rancher/rke2-charts/raw/main/assets/harvester-cloud-provider/harvester-csi-driver-{{.HarvesterCSIDriverChartVersion}}.tgz) |
| rke2-snapshot-controller | [{{.SnapshotControllerChartVersion}}](https://github.com/rancher/rke2-charts/raw/main/assets/rke2-snapshot-controller/rke2-snapshot-controller-{{.SnapshotControllerChartVersion}}.tgz) |
| rke2-snapshot-controller-crd | [{{.SnapshotControllerCRDChartVersion}}](https://github.com/rancher/rke2-charts/raw/main/assets/rke2-snapshot-controller/rke2-snapshot-controller-crd-{{.SnapshotControllerCRDChartVersion}}.tgz) |
| rke2-snapshot-validation-webhook | [{{.SnapshotValidationWebhookChartVersion}}](https://github.com/rancher/rke2-charts/raw/main/assets/rke2-snapshot-validation-webhook/rke2-snapshot-validation-webhook-{{.SnapshotValidationWebhookChartVersion}}.tgz) |
## Packaged Component Versions
| Component | Version |
| --- | --- |
| Kubernetes | [{{.K8sVersion}}](https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG-{{.MajorMinor}}.md#{{.ChangeLogVersion}}) |
| Etcd | [{{.EtcdVersion}}](https://github.com/k3s-io/etcd/releases/tag/{{.EtcdVersion}}) |
| Containerd | [{{.ContainerdVersion}}](https://github.com/k3s-io/containerd/releases/tag/{{.ContainerdVersion}}) |
| Runc | [{{.RuncVersion}}](https://github.com/opencontainers/runc/releases/tag/{{.RuncVersion}}) |
| Metrics-server | [{{.MetricsServerVersion}}](https://github.com/kubernetes-sigs/metrics-server/releases/tag/{{.MetricsServerVersion}}) |
| CoreDNS | [{{.CoreDNSVersion}}](https://github.com/coredns/coredns/releases/tag/{{.CoreDNSVersion}}) |
| Ingress-Nginx | [{{.IngressNginxVersion}}](https://github.com/rancher/ingress-nginx/releases/tag/{{.IngressNginxVersion}}) |
| Helm-controller | [{{.HelmControllerVersion}}](https://github.com/k3s-io/helm-controller/releases/tag/{{.HelmControllerVersion}}) |
### Available CNIs
| Component | Version | FIPS Compliant |
| --- | --- | --- |
| Canal (Default) | [Flannel {{.FlannelVersion}}](https://github.com/flannel-io/flannel/releases/tag/{{.FlannelVersion}})<br/>[Calico {{.CanalCalicoVersion}}]({{.CanalCalicoURL}}) | Yes |
| Calico | [{{.CalicoVersion}}]({{.CalicoURL}}) | No |
| Cilium | [{{.CiliumVersion}}](https://github.com/cilium/cilium/releases/tag/{{.CiliumVersion}}) | No |
| Multus | [{{.MultusVersion}}](https://github.com/k8snetworkplumbingwg/multus-cni/releases/tag/{{.MultusVersion}}) | No |
## Helpful Links
As always, we welcome and appreciate feedback from our community of users. Please feel free to:
- [Open issues here](https://github.com/rancher/rke2/issues/new)
- [Join our Slack channel](https://slack.rancher.io/)
- [Check out our documentation](https://docs.rke2.io) for guidance on how to get started.
{{ end }}`
const k3sReleaseNoteTemplate = `
{{- define "k3s" -}}
<!-- {{.Milestone}} -->
This release updates Kubernetes to {{.K8sVersion}}, and fixes a number of issues.
For more details on what's new, see the [Kubernetes release notes](https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG-{{.MajorMinor}}.md#changelog-since-{{.ChangeLogSince}}).
{{ template "changelog" . }}
## Embedded Component Versions
| Component | Version |
|---|---|
| Kubernetes | [{{.K8sVersion}}](https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG-{{.MajorMinor}}.md#{{.ChangeLogVersion}}) |
| Kine | [{{.KineVersion}}](https://github.com/k3s-io/kine/releases/tag/{{.KineVersion}}) |
| SQLite | [{{.SQLiteVersion}}](https://sqlite.org/releaselog/{{.SQLiteVersionReplaced}}.html) |
| Etcd | [{{.EtcdVersion}}](https://github.com/k3s-io/etcd/releases/tag/{{.EtcdVersion}}) |
| Containerd | [{{.ContainerdVersion}}](https://github.com/k3s-io/containerd/releases/tag/{{.ContainerdVersion}}) |
| Runc | [{{.RuncVersion}}](https://github.com/opencontainers/runc/releases/tag/{{.RuncVersion}}) |
| Flannel | [{{.FlannelVersion}}](https://github.com/flannel-io/flannel/releases/tag/{{.FlannelVersion}}) |
| Metrics-server | [{{.MetricsServerVersion}}](https://github.com/kubernetes-sigs/metrics-server/releases/tag/{{.MetricsServerVersion}}) |
| Traefik | [v{{.TraefikVersion}}](https://github.com/traefik/traefik/releases/tag/v{{.TraefikVersion}}) |
| CoreDNS | [v{{.CoreDNSVersion}}](https://github.com/coredns/coredns/releases/tag/v{{.CoreDNSVersion}}) |
| Helm-controller | [{{.HelmControllerVersion}}](https://github.com/k3s-io/helm-controller/releases/tag/{{.HelmControllerVersion}}) |
| Local-path-provisioner | [{{.LocalPathProvisionerVersion}}](https://github.com/rancher/local-path-provisioner/releases/tag/{{.LocalPathProvisionerVersion}}) |
## Helpful Links
As always, we welcome and appreciate feedback from our community of users. Please feel free to:
- [Open issues here](https://github.com/rancher/k3s/issues/new/choose)
- [Join our Slack channel](https://slack.rancher.io/)
- [Check out our documentation](https://rancher.com/docs/k3s/latest/en/) for guidance on how to get started or to dive deep into K3s.
- [Read how you can contribute here](https://github.com/rancher/k3s/blob/master/CONTRIBUTING.md)
{{ end }}`
const dashboardReleaseNoteTemplate = `
{{- define "%s" -}}
<!-- {{.Milestone}} -->
{{ template "changelog" . }}
{{ end }}`