mirror of
https://github.com/labring/sealos.git
synced 2026-08-29 01:39:49 +08:00
fix(lifecycle): support Kubernetes 1.33 upgrade flow (#6929)
* fix: wait for registry before pulling kubeadm images * fix: support kubeadm upgrade to newer minors * fix: recover kubelet config during upgrades * fix: handle kubelet upgrade recovery edge cases * fix: refresh rootfs sealctl before use * fix: tolerate existing temporary registry * fix: ignore kubeadm upgrade health check job * fix(kubernetes): align cert identity handling with kubeadm v1.29+ (#83) * build(lifecycle): auto-detect C compiler and CC Add CC and CC_<platform> Makefile options and implement compiler resolution for CGO-enabled sealos/sealctl builds. Update DEVELOPGUIDE with usage and a cross-compile example. Signed-off-by: Yun Pan <dinoallo@netc.it> * fix(kubernetes): align cert identity handling with kubeadm v1.29+ - migrate remote control-plane certs across the v1.29 identity boundary - repair remote admin.conf on all masters during upgrade - keep local sealos PKI and kubeconfigs aligned with the v1.29+ model - keep super-admin.conf local-only during renew and upgrade flows - preserve pre-v1.31 long-lived cert policy and use v1beta4 fields on v1.31+ - fix v1.31 remote admin.conf migration config distribution across masters Signed-off-by: Yun Pan <dinoallo@netc.it> --------- Signed-off-by: Yun Pan <dinoallo@netc.it> * fix(lifecycle): sync and upgrade codex * fix: cert and add timeout for cluster role binding create --------- Signed-off-by: Yun Pan <dinoallo@netc.it> Co-authored-by: Yun Pan <dinoallo@netc.it>
This commit is contained in:
@@ -173,4 +173,7 @@ make build
|
||||
|
||||
1. clone code slow, your can use ghproxy: `git clone https://ghproxy.com/https://github.com/labring/sealos`
|
||||
2. build download package slow, you can use goproxy: `go env -w GOPROXY=https://goproxy.cn,direct && make build`
|
||||
3. `cgo: C compiler "x86_64-linux-gnu-gcc" not found: exec: "x86_64-linux-gnu-gcc": executable file not found in $PATH` you need install gnu-gcc, like: `apt-get install build-essential` or `yum -y install gcc-c++-x86_64-linux-gnu`
|
||||
3. Build of `sealos` or `sealctl` fails with a C compiler error.
|
||||
For native Linux builds, the make rules now auto-detect an available host compiler from `cc`, `gcc`, or `clang`.
|
||||
For cross-compiling CGO-enabled binaries, install the target toolchain or override it explicitly, for example:
|
||||
`make build.multiarch BINS="sealos" CC_linux_arm64=aarch64-linux-gnu-gcc`
|
||||
|
||||
@@ -43,6 +43,12 @@ Options:
|
||||
This option is available when using: make {build}.multiarch
|
||||
Example: make build.multiarch PLATFORMS="linux_arm64 linux_amd64"
|
||||
|
||||
CC Override the C compiler for CGO-enabled binaries in single-platform builds.
|
||||
Example: make build BINS="sealos" CC=gcc
|
||||
|
||||
CC_<platform> Override the C compiler for a specific platform in multi-platform builds.
|
||||
Example: make build.multiarch BINS="sealos" CC_linux_arm64=aarch64-linux-gnu-gcc
|
||||
|
||||
V Set to 1 enable verbose build. Default is 0.
|
||||
endef
|
||||
export USAGE_OPTIONS
|
||||
|
||||
@@ -15,8 +15,10 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/labring/sealos/pkg/runtime"
|
||||
|
||||
@@ -32,6 +34,8 @@ import (
|
||||
|
||||
func newCertCmd() *cobra.Command {
|
||||
var altNames []string
|
||||
var renewTargets []string
|
||||
var groups []string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "cert",
|
||||
@@ -46,8 +50,19 @@ func newCertCmd() *cobra.Command {
|
||||
1. sealos cert --alt-names 39.105.169.253
|
||||
2. edit .kube/config, set the apiserver address as 39.105.169.253, (don't forget to open the security group port for 6443, if you using public cloud)
|
||||
3. kubectl get pod, to check if it works or not
|
||||
`,
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(renewTargets) != 0 {
|
||||
if len(altNames) != 0 {
|
||||
return errors.New("--alt-names and --renew cannot be used together")
|
||||
}
|
||||
} else if len(altNames) == 0 {
|
||||
return errors.New("--alt-names is required unless --renew is set")
|
||||
}
|
||||
if cmd.Flags().Changed("groups") && len(renewTargets) == 0 {
|
||||
return errors.New("--groups requires --renew")
|
||||
}
|
||||
|
||||
processor.SyncNewVersionConfig(clusterName)
|
||||
|
||||
clusterPath := constants.Clusterfile(clusterName)
|
||||
@@ -84,15 +99,44 @@ func newCertCmd() *cobra.Command {
|
||||
return fmt.Errorf("create runtime failed: %v", err)
|
||||
}
|
||||
if cm, ok := rt.(runtime.CertManager); ok {
|
||||
if len(renewTargets) != 0 {
|
||||
renewOpts := runtime.CertRenewOptions{Targets: renewTargets}
|
||||
if cmd.Flags().Changed("groups") {
|
||||
renewOpts.Groups = normalizeFlagValues(groups)
|
||||
}
|
||||
logger.Info("using %s cert renew implement on targets %v", cf.GetCluster().GetDistribution(), renewTargets)
|
||||
return cm.Renew(renewOpts)
|
||||
}
|
||||
logger.Info("using %s cert update implement", cf.GetCluster().GetDistribution())
|
||||
return cm.UpdateCertSANs(altNames)
|
||||
}
|
||||
if len(renewTargets) != 0 {
|
||||
return fmt.Errorf("renew targets %v are not supported for distribution %s", renewTargets, cf.GetCluster().GetDistribution())
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&clusterName, "cluster", "c", "default", "name of cluster to applied exec action")
|
||||
cmd.Flags().StringSliceVar(&altNames, "alt-names", []string{}, "add extra Subject Alternative Names for certs, domain or ip, eg. sealos.io or 10.103.97.2")
|
||||
_ = cmd.MarkFlagRequired("alt-names")
|
||||
cmd.Flags().StringSliceVar(&renewTargets, "renew", nil, "renew local cert targets; local admin.conf is synced to node $HOME/.kube/config, super-admin.conf stays local only")
|
||||
cmd.Flags().StringSliceVar(&groups, "groups", nil, "override admin kubeconfig certificate groups when renewing admin.conf or all")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func normalizeFlagValues(values []string) []string {
|
||||
normalized := make([]string, 0, len(values))
|
||||
seen := make(map[string]struct{}, len(values))
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[value]; ok {
|
||||
continue
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
normalized = append(normalized, value)
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
@@ -50,7 +50,10 @@ func (a *registryApplier) Apply(ctx Context, host string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return ctx.GetExecer().CmdAsync(host, ctx.GetBash().InitRegistryBash(host))
|
||||
if err := ctx.GetExecer().CmdAsync(host, ctx.GetBash().InitRegistryBash(host)); err != nil {
|
||||
return err
|
||||
}
|
||||
return helpers.WaitRegistryReady(ctx.GetExecer(), host, rc.Domain, rc.Port)
|
||||
}
|
||||
|
||||
func (*registryApplier) Undo(ctx Context, host string) error {
|
||||
|
||||
@@ -20,13 +20,43 @@ import "fmt"
|
||||
|
||||
// GenerateCert generate all cert.
|
||||
func GenerateCert(certPATH, certEtcdPATH string, altNames []string, hostIP, hostName, serviceCIRD, DNSDomain string) error {
|
||||
certConfig, err := NewSealosCertMetaData(certPATH, certEtcdPATH, altNames, serviceCIRD, hostName, hostIP, DNSDomain)
|
||||
return GenerateCertForKubeVersion(certPATH, certEtcdPATH, altNames, hostIP, hostName, serviceCIRD, DNSDomain, "")
|
||||
}
|
||||
|
||||
func GenerateCertForKubeVersion(certPATH, certEtcdPATH string, altNames []string, hostIP, hostName, serviceCIRD, DNSDomain, kubeVersion string) error {
|
||||
certConfig, err := NewSealosCertMetaDataForKubeVersion(certPATH, certEtcdPATH, altNames, serviceCIRD, hostName, hostIP, DNSDomain, kubeVersion)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generator cert config failed %v", err)
|
||||
}
|
||||
return certConfig.GenerateAll()
|
||||
}
|
||||
|
||||
// RenewCert regenerates all local PKI files, including root CAs and leaf certificates.
|
||||
func RenewCert(certPATH, certEtcdPATH string, altNames []string, hostIP, hostName, serviceCIRD, DNSDomain string) error {
|
||||
return RenewCertForKubeVersion(certPATH, certEtcdPATH, altNames, hostIP, hostName, serviceCIRD, DNSDomain, "")
|
||||
}
|
||||
|
||||
func RenewCertForKubeVersion(certPATH, certEtcdPATH string, altNames []string, hostIP, hostName, serviceCIRD, DNSDomain, kubeVersion string) error {
|
||||
certConfig, err := NewSealosCertMetaDataForKubeVersion(certPATH, certEtcdPATH, altNames, serviceCIRD, hostName, hostIP, DNSDomain, kubeVersion)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generator cert config failed %v", err)
|
||||
}
|
||||
return certConfig.RenewAll()
|
||||
}
|
||||
|
||||
// RenewLeafCerts regenerates local leaf certificates while preserving the existing CAs.
|
||||
func RenewLeafCerts(certPATH, certEtcdPATH string, altNames []string, hostIP, hostName, serviceCIRD, DNSDomain string) error {
|
||||
return RenewLeafCertsForKubeVersion(certPATH, certEtcdPATH, altNames, hostIP, hostName, serviceCIRD, DNSDomain, "")
|
||||
}
|
||||
|
||||
func RenewLeafCertsForKubeVersion(certPATH, certEtcdPATH string, altNames []string, hostIP, hostName, serviceCIRD, DNSDomain, kubeVersion string) error {
|
||||
certConfig, err := NewSealosCertMetaDataForKubeVersion(certPATH, certEtcdPATH, altNames, serviceCIRD, hostName, hostIP, DNSDomain, kubeVersion)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generator cert config failed %v", err)
|
||||
}
|
||||
return certConfig.RenewLeafCerts()
|
||||
}
|
||||
|
||||
func GenerateRegistryCert(registryCertPath string, BaseName string) error {
|
||||
regCertConfig := Config{
|
||||
Path: registryCertPath,
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright © 2026 sealos.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cert
|
||||
|
||||
import "github.com/Masterminds/semver/v3"
|
||||
|
||||
const (
|
||||
legacyPrivilegedGroup = "system:masters"
|
||||
clusterAdminsGroup = "kubeadm:cluster-admins"
|
||||
)
|
||||
|
||||
var kube1290 = semver.MustParse("v1.29.0")
|
||||
|
||||
type localIdentityModel struct {
|
||||
DefaultAdminOrganizations []string
|
||||
IncludeSuperAdminKubeConfig bool
|
||||
SuperAdminClientName string
|
||||
SuperAdminOrganizations []string
|
||||
APIServerKubeletOrganizations []string
|
||||
APIServerEtcdClientOrganizations []string
|
||||
EtcdHealthcheckOrganizations []string
|
||||
}
|
||||
|
||||
func resolveLocalIdentityModel(kubeVersion string) localIdentityModel {
|
||||
if kubeVersion != "" {
|
||||
if version, err := semver.NewVersion(kubeVersion); err == nil && !version.LessThan(kube1290) {
|
||||
return localIdentityModel{
|
||||
DefaultAdminOrganizations: []string{clusterAdminsGroup},
|
||||
IncludeSuperAdminKubeConfig: true,
|
||||
SuperAdminClientName: "kubernetes-super-admin",
|
||||
SuperAdminOrganizations: []string{legacyPrivilegedGroup},
|
||||
APIServerKubeletOrganizations: []string{clusterAdminsGroup},
|
||||
APIServerEtcdClientOrganizations: nil,
|
||||
EtcdHealthcheckOrganizations: nil,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return localIdentityModel{
|
||||
DefaultAdminOrganizations: []string{legacyPrivilegedGroup},
|
||||
IncludeSuperAdminKubeConfig: false,
|
||||
SuperAdminClientName: "kubernetes-super-admin",
|
||||
SuperAdminOrganizations: []string{legacyPrivilegedGroup},
|
||||
APIServerKubeletOrganizations: []string{legacyPrivilegedGroup},
|
||||
APIServerEtcdClientOrganizations: []string{legacyPrivilegedGroup},
|
||||
EtcdHealthcheckOrganizations: []string{legacyPrivilegedGroup},
|
||||
}
|
||||
}
|
||||
|
||||
func usesClusterAdminsIdentityModel(kubeVersion string) bool {
|
||||
return resolveLocalIdentityModel(kubeVersion).IncludeSuperAdminKubeConfig
|
||||
}
|
||||
|
||||
func UsesClusterAdminsIdentityModel(kubeVersion string) bool {
|
||||
return usesClusterAdminsIdentityModel(kubeVersion)
|
||||
}
|
||||
@@ -68,7 +68,7 @@ func CaList(CertPath, CertEtcdPath string) []Config {
|
||||
}
|
||||
}
|
||||
|
||||
func List(CertPath, CertEtcdPath string) []Config {
|
||||
func List(CertPath, CertEtcdPath string, identity localIdentityModel) []Config {
|
||||
return []Config{
|
||||
{
|
||||
Path: CertPath,
|
||||
@@ -97,7 +97,7 @@ func List(CertPath, CertEtcdPath string) []Config {
|
||||
BaseName: "apiserver-kubelet-client",
|
||||
CAName: "kubernetes",
|
||||
CommonName: "kube-apiserver-kubelet-client",
|
||||
Organization: []string{"system:masters"},
|
||||
Organization: identity.APIServerKubeletOrganizations,
|
||||
Year: 100,
|
||||
AltNames: AltNames{},
|
||||
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||
@@ -119,7 +119,7 @@ func List(CertPath, CertEtcdPath string) []Config {
|
||||
BaseName: "apiserver-etcd-client",
|
||||
CAName: "etcd-ca",
|
||||
CommonName: "kube-apiserver-etcd-client",
|
||||
Organization: []string{"system:masters"},
|
||||
Organization: identity.APIServerEtcdClientOrganizations,
|
||||
Year: 100,
|
||||
AltNames: AltNames{},
|
||||
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||
@@ -152,7 +152,7 @@ func List(CertPath, CertEtcdPath string) []Config {
|
||||
BaseName: "healthcheck-client",
|
||||
CAName: "etcd-ca",
|
||||
CommonName: "kube-etcd-healthcheck-client",
|
||||
Organization: []string{"system:masters"},
|
||||
Organization: identity.EtcdHealthcheckOrganizations,
|
||||
Year: 100,
|
||||
AltNames: AltNames{},
|
||||
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||
@@ -166,6 +166,7 @@ type SealosCertMetaData struct {
|
||||
NodeName string
|
||||
NodeIP string
|
||||
DNSDomain string
|
||||
identity localIdentityModel
|
||||
//证书生成的位置
|
||||
CertPath string
|
||||
CertEtcdPath string
|
||||
@@ -183,10 +184,15 @@ const (
|
||||
|
||||
// apiServerIPAndDomains = MasterIP + VIP + CertSANS 暂时只有apiserver, 记得把cluster.local后缀加到apiServerIPAndDOmas里先
|
||||
func NewSealosCertMetaData(certPATH, certEtcdPATH string, apiServerIPAndDomains []string, SvcCIDR, nodeName, nodeIP, DNSDomain string) (*SealosCertMetaData, error) {
|
||||
return NewSealosCertMetaDataForKubeVersion(certPATH, certEtcdPATH, apiServerIPAndDomains, SvcCIDR, nodeName, nodeIP, DNSDomain, "")
|
||||
}
|
||||
|
||||
func NewSealosCertMetaDataForKubeVersion(certPATH, certEtcdPATH string, apiServerIPAndDomains []string, SvcCIDR, nodeName, nodeIP, DNSDomain, kubeVersion string) (*SealosCertMetaData, error) {
|
||||
data := &SealosCertMetaData{}
|
||||
data.CertPath = certPATH
|
||||
data.CertEtcdPath = certEtcdPATH
|
||||
data.DNSDomain = DNSDomain
|
||||
data.identity = resolveLocalIdentityModel(kubeVersion)
|
||||
data.APIServer.IPs = make(map[string]net.IP)
|
||||
data.APIServer.DNSNames = make(map[string]string)
|
||||
|
||||
@@ -278,8 +284,16 @@ func (meta *SealosCertMetaData) generatorServiceAccountKeyPaire() error {
|
||||
}
|
||||
|
||||
func (meta *SealosCertMetaData) GenerateAll() error {
|
||||
return meta.generateAll(false)
|
||||
}
|
||||
|
||||
func (meta *SealosCertMetaData) RenewAll() error {
|
||||
return meta.generateAll(true)
|
||||
}
|
||||
|
||||
func (meta *SealosCertMetaData) RenewLeafCerts() error {
|
||||
cas := CaList(meta.CertPath, meta.CertEtcdPath)
|
||||
certs := List(meta.CertPath, meta.CertEtcdPath)
|
||||
certs := List(meta.CertPath, meta.CertEtcdPath, meta.identity)
|
||||
meta.apiServerAltName(&certs)
|
||||
meta.etcdAltAndCommonName(&certs)
|
||||
_ = meta.generatorServiceAccountKeyPaire()
|
||||
@@ -287,7 +301,63 @@ func (meta *SealosCertMetaData) GenerateAll() error {
|
||||
CACerts := map[string]*x509.Certificate{}
|
||||
CAKeys := map[string]crypto.Signer{}
|
||||
for _, ca := range cas {
|
||||
caCert, caKey, err := NewCaCertAndKey(ca)
|
||||
caCert, caKey, err := LoadCaCertAndKeyFromDisk(ca)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
CACerts[ca.CommonName] = caCert
|
||||
CAKeys[ca.CommonName] = caKey
|
||||
}
|
||||
|
||||
for _, cert := range certs {
|
||||
caCert, ok := CACerts[cert.CAName]
|
||||
if !ok {
|
||||
return fmt.Errorf("root ca cert not found %s", cert.CAName)
|
||||
}
|
||||
caKey, ok := CAKeys[cert.CAName]
|
||||
if !ok {
|
||||
return fmt.Errorf("root ca key not found %s", cert.CAName)
|
||||
}
|
||||
|
||||
Cert, Key, err := NewCaCertAndKeyFromRoot(cert, caCert, caKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = WriteCertAndKey(cert.Path, cert.BaseName, Cert, Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (meta *SealosCertMetaData) generateAll(force bool) error {
|
||||
cas := CaList(meta.CertPath, meta.CertEtcdPath)
|
||||
certs := List(meta.CertPath, meta.CertEtcdPath, meta.identity)
|
||||
meta.apiServerAltName(&certs)
|
||||
meta.etcdAltAndCommonName(&certs)
|
||||
_ = meta.generatorServiceAccountKeyPaire()
|
||||
|
||||
CACerts := map[string]*x509.Certificate{}
|
||||
CAKeys := map[string]crypto.Signer{}
|
||||
for _, ca := range cas {
|
||||
var (
|
||||
caCert *x509.Certificate
|
||||
caKey crypto.Signer
|
||||
err error
|
||||
)
|
||||
if force {
|
||||
caKey, err = NewPrivateKey(x509.UnknownPublicKeyAlgorithm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
caCert, err = NewSelfSignedCACert(caKey, ca.CommonName, ca.Organization, ca.Year)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
caCert, caKey, err = NewCaCertAndKey(ca)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
package cert
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
certutil "k8s.io/client-go/util/cert"
|
||||
)
|
||||
|
||||
func TestGenerateAll(t *testing.T) {
|
||||
@@ -43,3 +47,170 @@ func TestGenerateAll(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenewAllRotatesRootCA(t *testing.T) {
|
||||
basePath := t.TempDir()
|
||||
certPath := filepath.Join(basePath, "pki")
|
||||
etcdPath := filepath.Join(certPath, "etcd")
|
||||
|
||||
meta, err := NewSealosCertMetaData(certPath, etcdPath, []string{"test.com", "192.168.1.2"}, "10.64.0.0/10", "master1", "172.27.139.11", "cluster.local")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = meta.GenerateAll(); err != nil {
|
||||
t.Fatalf("GenerateAll() error = %v", err)
|
||||
}
|
||||
|
||||
oldPEM, err := os.ReadFile(filepath.Join(certPath, "ca.crt"))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(old ca.crt) error = %v", err)
|
||||
}
|
||||
oldCerts, err := certutil.ParseCertsPEM(oldPEM)
|
||||
if err != nil || len(oldCerts) == 0 {
|
||||
t.Fatalf("ParseCertsPEM(old ca.crt) error = %v", err)
|
||||
}
|
||||
|
||||
if err = meta.RenewAll(); err != nil {
|
||||
t.Fatalf("RenewAll() error = %v", err)
|
||||
}
|
||||
|
||||
newPEM, err := os.ReadFile(filepath.Join(certPath, "ca.crt"))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(new ca.crt) error = %v", err)
|
||||
}
|
||||
newCerts, err := certutil.ParseCertsPEM(newPEM)
|
||||
if err != nil || len(newCerts) == 0 {
|
||||
t.Fatalf("ParseCertsPEM(new ca.crt) error = %v", err)
|
||||
}
|
||||
|
||||
if string(oldPEM) == string(newPEM) {
|
||||
t.Fatalf("expected root CA certificate to change after RenewAll()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateAllForV129UsesUpdatedIdentityModel(t *testing.T) {
|
||||
basePath := t.TempDir()
|
||||
certPath := filepath.Join(basePath, "pki")
|
||||
etcdPath := filepath.Join(certPath, "etcd")
|
||||
|
||||
meta, err := NewSealosCertMetaDataForKubeVersion(certPath, etcdPath, []string{"test.com", "192.168.1.2"}, "10.64.0.0/10", "master1", "172.27.139.11", "cluster.local", kubeVersion129)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = meta.GenerateAll(); err != nil {
|
||||
t.Fatalf("GenerateAll() error = %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
wantOrgs []string
|
||||
}{
|
||||
{
|
||||
name: "apiserver-kubelet-client",
|
||||
path: filepath.Join(certPath, "apiserver-kubelet-client.crt"),
|
||||
wantOrgs: []string{clusterAdminsGroup},
|
||||
},
|
||||
{
|
||||
name: "apiserver-etcd-client",
|
||||
path: filepath.Join(certPath, "apiserver-etcd-client.crt"),
|
||||
wantOrgs: nil,
|
||||
},
|
||||
{
|
||||
name: "etcd-healthcheck-client",
|
||||
path: filepath.Join(etcdPath, "healthcheck-client.crt"),
|
||||
wantOrgs: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
certs, err := certutil.CertsFromFile(tt.path)
|
||||
if err != nil {
|
||||
t.Fatalf("CertsFromFile(%s) error = %v", tt.name, err)
|
||||
}
|
||||
if len(certs) == 0 {
|
||||
t.Fatalf("expected certificate for %s", tt.name)
|
||||
}
|
||||
if got := certs[0].Subject.Organization; len(got) != len(tt.wantOrgs) {
|
||||
t.Fatalf("%s organizations = %v, want %v", tt.name, got, tt.wantOrgs)
|
||||
} else {
|
||||
for i := range got {
|
||||
if got[i] != tt.wantOrgs[i] {
|
||||
t.Fatalf("%s organizations = %v, want %v", tt.name, got, tt.wantOrgs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenewLeafCertsForV129KeepsCAAndUsesUpdatedIdentityModel(t *testing.T) {
|
||||
basePath := t.TempDir()
|
||||
certPath := filepath.Join(basePath, "pki")
|
||||
etcdPath := filepath.Join(certPath, "etcd")
|
||||
|
||||
meta, err := NewSealosCertMetaData(certPath, etcdPath, []string{"test.com", "192.168.1.2"}, "10.64.0.0/10", "master1", "172.27.139.11", "cluster.local")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = meta.GenerateAll(); err != nil {
|
||||
t.Fatalf("GenerateAll() error = %v", err)
|
||||
}
|
||||
|
||||
oldCA, err := os.ReadFile(filepath.Join(certPath, "ca.crt"))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(ca.crt) error = %v", err)
|
||||
}
|
||||
|
||||
if err = RenewLeafCertsForKubeVersion(certPath, etcdPath, []string{"test.com", "192.168.1.2"}, "172.27.139.11", "master1", "10.64.0.0/10", "cluster.local", kubeVersion129); err != nil {
|
||||
t.Fatalf("RenewLeafCertsForKubeVersion() error = %v", err)
|
||||
}
|
||||
|
||||
newCA, err := os.ReadFile(filepath.Join(certPath, "ca.crt"))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(ca.crt) after renew error = %v", err)
|
||||
}
|
||||
if string(oldCA) != string(newCA) {
|
||||
t.Fatal("expected RenewLeafCertsForKubeVersion() to preserve the existing CA")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
wantOrgs []string
|
||||
}{
|
||||
{
|
||||
name: "apiserver-kubelet-client",
|
||||
path: filepath.Join(certPath, "apiserver-kubelet-client.crt"),
|
||||
wantOrgs: []string{clusterAdminsGroup},
|
||||
},
|
||||
{
|
||||
name: "apiserver-etcd-client",
|
||||
path: filepath.Join(certPath, "apiserver-etcd-client.crt"),
|
||||
wantOrgs: nil,
|
||||
},
|
||||
{
|
||||
name: "etcd-healthcheck-client",
|
||||
path: filepath.Join(etcdPath, "healthcheck-client.crt"),
|
||||
wantOrgs: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
certs, err := certutil.CertsFromFile(tt.path)
|
||||
if err != nil {
|
||||
t.Fatalf("CertsFromFile(%s) error = %v", tt.name, err)
|
||||
}
|
||||
if len(certs) == 0 {
|
||||
t.Fatalf("expected certificate for %s", tt.name)
|
||||
}
|
||||
if got := certs[0].Subject.Organization; len(got) != len(tt.wantOrgs) {
|
||||
t.Fatalf("%s organizations = %v, want %v", tt.name, got, tt.wantOrgs)
|
||||
} else {
|
||||
for i := range got {
|
||||
if got[i] != tt.wantOrgs[i] {
|
||||
t.Fatalf("%s organizations = %v, want %v", tt.name, got, tt.wantOrgs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
@@ -50,35 +51,124 @@ type kubeConfigSpec struct {
|
||||
ClientCertAuth *clientCertAuth
|
||||
}
|
||||
|
||||
const adminKubeConfigFileName = "admin.conf"
|
||||
const (
|
||||
controllerManagerKubeConfigFileName = "controller-manager.conf"
|
||||
schedulerKubeConfigFileName = "scheduler.conf"
|
||||
kubeletKubeConfigFileName = "kubelet.conf"
|
||||
superAdminKubeConfigFileName = "super-admin.conf"
|
||||
)
|
||||
|
||||
// CreateJoinControlPlaneKubeConfigFiles will create and write to disk the kubeconfig files required by kubeadm
|
||||
// join --control-plane workflow, plus the admin kubeconfig file used by the administrator and kubeadm itself; the
|
||||
// kubelet.conf file must not be created because it will be created and signed by the kubelet TLS bootstrap process.
|
||||
// If any kubeconfig files already exists, it used only if evaluated equal; otherwise an error is returned.
|
||||
func CreateJoinControlPlaneKubeConfigFiles(outDir string, cfg Config, nodeName, controlPlaneEndpoint, clusterName string) error {
|
||||
return CreateJoinControlPlaneKubeConfigFilesForKubeVersion(outDir, cfg, nodeName, controlPlaneEndpoint, clusterName, "")
|
||||
}
|
||||
|
||||
func CreateJoinControlPlaneKubeConfigFilesForKubeVersion(outDir string, cfg Config, nodeName, controlPlaneEndpoint, clusterName, kubeVersion string) error {
|
||||
identity := resolveLocalIdentityModel(kubeVersion)
|
||||
kubeConfigFileNames := []string{
|
||||
adminKubeConfigFileName,
|
||||
controllerManagerKubeConfigFileName,
|
||||
schedulerKubeConfigFileName,
|
||||
kubeletKubeConfigFileName,
|
||||
}
|
||||
if identity.IncludeSuperAdminKubeConfig {
|
||||
kubeConfigFileNames = append(kubeConfigFileNames, superAdminKubeConfigFileName)
|
||||
}
|
||||
return createKubeConfigFiles(
|
||||
outDir,
|
||||
cfg,
|
||||
nodeName,
|
||||
controlPlaneEndpoint,
|
||||
clusterName,
|
||||
"admin.conf",
|
||||
"controller-manager.conf",
|
||||
"scheduler.conf",
|
||||
"kubelet.conf", //master1上的kubeconfig跟随三个组件一起生成
|
||||
identity,
|
||||
kubeConfigFileNames...,
|
||||
)
|
||||
}
|
||||
|
||||
// RenewAdminKubeConfigFile re-signs and overwrites the local admin kubeconfig used by sealos.
|
||||
func RenewAdminKubeConfigFile(outDir string, cfg Config, controlPlaneEndpoint, clusterName string, organizations []string) error {
|
||||
return RenewAdminKubeConfigFileForKubeVersion(outDir, cfg, controlPlaneEndpoint, clusterName, organizations, "")
|
||||
}
|
||||
|
||||
func RenewAdminKubeConfigFileForKubeVersion(outDir string, cfg Config, controlPlaneEndpoint, clusterName string, organizations []string, kubeVersion string) error {
|
||||
spec, err := getAdminKubeConfigSpec(cfg, controlPlaneEndpoint, organizations, resolveLocalIdentityModel(kubeVersion))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config, err := buildKubeConfigFromSpec(spec, clusterName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = os.MkdirAll(outDir, 0o700); err != nil {
|
||||
return fmt.Errorf("failed to create kubeconfig output directory %q: %w", outDir, err)
|
||||
}
|
||||
|
||||
kubeConfigFilePath := filepath.Join(outDir, adminKubeConfigFileName)
|
||||
logger.Debug("[kubeconfig] Renewing %q kubeconfig file\n", kubeConfigFilePath)
|
||||
if err = WriteToDisk(kubeConfigFilePath, config); err != nil {
|
||||
return fmt.Errorf("failed to save kubeconfig file %q on disk: %w", kubeConfigFilePath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RenewKubeConfigFiles re-signs and overwrites the selected local kubeconfig files used by sealos.
|
||||
func RenewKubeConfigFiles(outDir string, cfg Config, nodeName, controlPlaneEndpoint, clusterName string, adminOrganizations []string, kubeConfigFileNames ...string) error {
|
||||
return RenewKubeConfigFilesForKubeVersion(outDir, cfg, nodeName, controlPlaneEndpoint, clusterName, adminOrganizations, "", kubeConfigFileNames...)
|
||||
}
|
||||
|
||||
func RenewKubeConfigFilesForKubeVersion(outDir string, cfg Config, nodeName, controlPlaneEndpoint, clusterName string, adminOrganizations []string, kubeVersion string, kubeConfigFileNames ...string) error {
|
||||
if len(kubeConfigFileNames) == 0 {
|
||||
return errors.New("at least one kubeconfig file must be specified")
|
||||
}
|
||||
|
||||
specs, err := getKubeConfigSpecs(cfg, nodeName, controlPlaneEndpoint, adminOrganizations, resolveLocalIdentityModel(kubeVersion))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = os.MkdirAll(outDir, 0o700); err != nil {
|
||||
return fmt.Errorf("failed to create kubeconfig output directory %q: %w", outDir, err)
|
||||
}
|
||||
|
||||
for _, kubeConfigFileName := range kubeConfigFileNames {
|
||||
spec, exists := specs[kubeConfigFileName]
|
||||
if !exists {
|
||||
return fmt.Errorf("couldn't retrieve KubeConfigSpec for %s", kubeConfigFileName)
|
||||
}
|
||||
|
||||
config, err := buildKubeConfigFromSpec(spec, clusterName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
kubeConfigFilePath := filepath.Join(outDir, kubeConfigFileName)
|
||||
logger.Debug("[kubeconfig] Renewing %q kubeconfig file\n", kubeConfigFilePath)
|
||||
if err = WriteToDisk(kubeConfigFilePath, config); err != nil {
|
||||
return fmt.Errorf("failed to save kubeconfig file %q on disk: %w", kubeConfigFilePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 方法没有被 ↑ 的方法调用,而是在cmd/kubeadm/app/cmd/phases/init/kubeconfig.go里调用
|
||||
// cmd/kubeadm/app/phases/kubeconfig/kubeconfig.go
|
||||
func CreateKubeConfigFile(kubeConfigFileName string, outDir string, cfg Config, nodeName, controlPlaneEndpoint, clusterName string) error {
|
||||
logger.Info("creating kubeconfig file for %s", kubeConfigFileName)
|
||||
return createKubeConfigFiles(outDir, cfg, kubeConfigFileName, nodeName, controlPlaneEndpoint, clusterName)
|
||||
return createKubeConfigFiles(outDir, cfg, nodeName, controlPlaneEndpoint, clusterName, resolveLocalIdentityModel(""), kubeConfigFileName)
|
||||
}
|
||||
|
||||
// createKubeConfigFiles creates all the requested kubeconfig files.
|
||||
// If kubeconfig files already exists, they are used only if evaluated equal; otherwise an error is returned.
|
||||
func createKubeConfigFiles(outDir string, cfg Config, nodeName, controlPlaneEndpoint, clusterName string, kubeConfigFileNames ...string) error { // gets the KubeConfigSpecs, actualized for the current InitConfiguration
|
||||
specs, err := getKubeConfigSpecs(cfg, nodeName, controlPlaneEndpoint)
|
||||
func createKubeConfigFiles(outDir string, cfg Config, nodeName, controlPlaneEndpoint, clusterName string, identity localIdentityModel, kubeConfigFileNames ...string) error { // gets the KubeConfigSpecs, actualized for the current InitConfiguration
|
||||
specs, err := getKubeConfigSpecs(cfg, nodeName, controlPlaneEndpoint, nil, identity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -105,9 +195,32 @@ func createKubeConfigFiles(outDir string, cfg Config, nodeName, controlPlaneEndp
|
||||
return nil
|
||||
}
|
||||
|
||||
func getAdminKubeConfigSpec(cfg Config, controlPlaneEndpoint string, organizations []string, identity localIdentityModel) (*kubeConfigSpec, error) {
|
||||
caCert, caKey, err := LoadCaCertAndKeyFromDisk(cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't create a kubeconfig; the CA files couldn't be loaded: %w", err)
|
||||
}
|
||||
|
||||
if len(controlPlaneEndpoint) == 0 {
|
||||
return nil, errors.New("controlPlaneEndpoint can not be empty")
|
||||
}
|
||||
|
||||
adminOrganizations := normalizeOrganizations(organizations, identity.DefaultAdminOrganizations)
|
||||
|
||||
return &kubeConfigSpec{
|
||||
CACert: caCert,
|
||||
APIServer: controlPlaneEndpoint,
|
||||
ClientName: "kubernetes-admin",
|
||||
ClientCertAuth: &clientCertAuth{
|
||||
CAKey: caKey,
|
||||
Organizations: adminOrganizations,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getKubeConfigSpecs returns all KubeConfigSpecs actualized to the context of the current InitConfiguration
|
||||
// NB. this methods holds the information about how kubeadm creates kubeconfig files.
|
||||
func getKubeConfigSpecs(cfg Config, nodeName, controlPlaneEndpoint string) (map[string]*kubeConfigSpec, error) {
|
||||
func getKubeConfigSpecs(cfg Config, nodeName, controlPlaneEndpoint string, adminOrganizations []string, identity localIdentityModel) (map[string]*kubeConfigSpec, error) {
|
||||
caCert, caKey, err := LoadCaCertAndKeyFromDisk(cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't create a kubeconfig; the CA files couldn't be loaded: %w", err)
|
||||
@@ -121,14 +234,16 @@ func getKubeConfigSpecs(cfg Config, nodeName, controlPlaneEndpoint string) (map[
|
||||
return nil, errors.New("controlPlaneEndpoint can not be empty")
|
||||
}
|
||||
|
||||
var kubeConfigSpec = map[string]*kubeConfigSpec{
|
||||
normalizedAdminOrganizations := normalizeOrganizations(adminOrganizations, identity.DefaultAdminOrganizations)
|
||||
|
||||
specs := map[string]*kubeConfigSpec{
|
||||
"admin.conf": {
|
||||
CACert: caCert,
|
||||
APIServer: controlPlaneEndpoint,
|
||||
ClientName: "kubernetes-admin",
|
||||
ClientCertAuth: &clientCertAuth{
|
||||
CAKey: caKey,
|
||||
Organizations: []string{"system:masters"},
|
||||
Organizations: normalizedAdminOrganizations,
|
||||
},
|
||||
},
|
||||
"kubelet.conf": {
|
||||
@@ -140,7 +255,7 @@ func getKubeConfigSpecs(cfg Config, nodeName, controlPlaneEndpoint string) (map[
|
||||
Organizations: []string{"system:nodes"},
|
||||
},
|
||||
},
|
||||
"controller-manager.conf": {
|
||||
controllerManagerKubeConfigFileName: {
|
||||
CACert: caCert,
|
||||
APIServer: controlPlaneEndpoint,
|
||||
ClientName: "system:kube-controller-manager",
|
||||
@@ -148,7 +263,7 @@ func getKubeConfigSpecs(cfg Config, nodeName, controlPlaneEndpoint string) (map[
|
||||
CAKey: caKey,
|
||||
},
|
||||
},
|
||||
"scheduler.conf": {
|
||||
schedulerKubeConfigFileName: {
|
||||
CACert: caCert,
|
||||
APIServer: controlPlaneEndpoint,
|
||||
ClientName: "system:kube-scheduler",
|
||||
@@ -157,8 +272,40 @@ func getKubeConfigSpecs(cfg Config, nodeName, controlPlaneEndpoint string) (map[
|
||||
},
|
||||
},
|
||||
}
|
||||
if identity.IncludeSuperAdminKubeConfig {
|
||||
specs[superAdminKubeConfigFileName] = &kubeConfigSpec{
|
||||
CACert: caCert,
|
||||
APIServer: controlPlaneEndpoint,
|
||||
ClientName: identity.SuperAdminClientName,
|
||||
ClientCertAuth: &clientCertAuth{
|
||||
CAKey: caKey,
|
||||
Organizations: identity.SuperAdminOrganizations,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return kubeConfigSpec, nil
|
||||
return specs, nil
|
||||
}
|
||||
|
||||
func normalizeOrganizations(organizations []string, defaultOrganizations []string) []string {
|
||||
if organizations == nil {
|
||||
return append([]string(nil), defaultOrganizations...)
|
||||
}
|
||||
|
||||
normalized := make([]string, 0, len(organizations))
|
||||
seen := make(map[string]struct{}, len(organizations))
|
||||
for _, organization := range organizations {
|
||||
organization = strings.TrimSpace(organization)
|
||||
if organization == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[organization]; ok {
|
||||
continue
|
||||
}
|
||||
seen[organization] = struct{}{}
|
||||
normalized = append(normalized, organization)
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
// buildKubeConfigFromSpec creates a kubeconfig object for the given kubeConfigSpec
|
||||
|
||||
@@ -0,0 +1,459 @@
|
||||
package cert
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
certutil "k8s.io/client-go/util/cert"
|
||||
)
|
||||
|
||||
const kubeVersion129 = "v1.29.0"
|
||||
|
||||
func TestRenewAdminKubeConfigFileCreatesAdminConfig(t *testing.T) {
|
||||
baseDir := t.TempDir()
|
||||
pkiDir := filepath.Join(baseDir, "pki")
|
||||
etcdDir := filepath.Join(pkiDir, "etcd")
|
||||
outDir := filepath.Join(baseDir, "etc")
|
||||
|
||||
if err := GenerateCert(
|
||||
pkiDir,
|
||||
etcdDir,
|
||||
[]string{"10.96.0.1", "apiserver.test"},
|
||||
"192.168.0.10",
|
||||
"master0",
|
||||
"10.64.0.0/10",
|
||||
"cluster.local",
|
||||
); err != nil {
|
||||
t.Fatalf("GenerateCert() error = %v", err)
|
||||
}
|
||||
|
||||
cfg := Config{
|
||||
Path: pkiDir,
|
||||
BaseName: "ca",
|
||||
}
|
||||
endpoint := "https://apiserver.test:6443"
|
||||
|
||||
if err := RenewAdminKubeConfigFile(outDir, cfg, endpoint, "kubernetes", nil); err != nil {
|
||||
t.Fatalf("RenewAdminKubeConfigFile() error = %v", err)
|
||||
}
|
||||
|
||||
adminConfig, err := clientcmd.LoadFromFile(filepath.Join(outDir, adminKubeConfigFileName))
|
||||
if err != nil {
|
||||
t.Fatalf("LoadFromFile() error = %v", err)
|
||||
}
|
||||
|
||||
authInfo := adminConfig.AuthInfos["kubernetes-admin"]
|
||||
if authInfo == nil {
|
||||
t.Fatalf("expected kubernetes-admin auth info to exist")
|
||||
}
|
||||
|
||||
certs, err := certutil.ParseCertsPEM(authInfo.ClientCertificateData)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseCertsPEM() error = %v", err)
|
||||
}
|
||||
if len(certs) == 0 {
|
||||
t.Fatalf("expected at least one client certificate")
|
||||
}
|
||||
|
||||
if got, want := certs[0].Subject.CommonName, "kubernetes-admin"; got != want {
|
||||
t.Fatalf("client certificate common name = %q, want %q", got, want)
|
||||
}
|
||||
if len(certs[0].Subject.Organization) != 1 || certs[0].Subject.Organization[0] != "system:masters" {
|
||||
t.Fatalf("client certificate organizations = %v, want [system:masters]", certs[0].Subject.Organization)
|
||||
}
|
||||
|
||||
currentCtx := adminConfig.Contexts[adminConfig.CurrentContext]
|
||||
if currentCtx == nil {
|
||||
t.Fatalf("expected current context %q to exist", adminConfig.CurrentContext)
|
||||
}
|
||||
cluster := adminConfig.Clusters[currentCtx.Cluster]
|
||||
if cluster == nil {
|
||||
t.Fatalf("expected cluster %q to exist", currentCtx.Cluster)
|
||||
}
|
||||
if got := cluster.Server; got != endpoint {
|
||||
t.Fatalf("cluster server = %q, want %q", got, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenewAdminKubeConfigFileForV129UsesClusterAdminsGroup(t *testing.T) {
|
||||
baseDir := t.TempDir()
|
||||
pkiDir := filepath.Join(baseDir, "pki")
|
||||
etcdDir := filepath.Join(pkiDir, "etcd")
|
||||
outDir := filepath.Join(baseDir, "etc")
|
||||
|
||||
if err := GenerateCertForKubeVersion(
|
||||
pkiDir,
|
||||
etcdDir,
|
||||
[]string{"10.96.0.1", "apiserver.test"},
|
||||
"192.168.0.10",
|
||||
"master0",
|
||||
"10.64.0.0/10",
|
||||
"cluster.local",
|
||||
kubeVersion129,
|
||||
); err != nil {
|
||||
t.Fatalf("GenerateCertForKubeVersion() error = %v", err)
|
||||
}
|
||||
|
||||
cfg := Config{
|
||||
Path: pkiDir,
|
||||
BaseName: "ca",
|
||||
}
|
||||
|
||||
if err := RenewAdminKubeConfigFileForKubeVersion(outDir, cfg, "https://apiserver.test:6443", "kubernetes", nil, kubeVersion129); err != nil {
|
||||
t.Fatalf("RenewAdminKubeConfigFileForKubeVersion() error = %v", err)
|
||||
}
|
||||
|
||||
adminConfig, err := clientcmd.LoadFromFile(filepath.Join(outDir, adminKubeConfigFileName))
|
||||
if err != nil {
|
||||
t.Fatalf("LoadFromFile() error = %v", err)
|
||||
}
|
||||
authInfo := adminConfig.AuthInfos["kubernetes-admin"]
|
||||
if authInfo == nil {
|
||||
t.Fatalf("expected kubernetes-admin auth info to exist")
|
||||
}
|
||||
|
||||
certs, err := certutil.ParseCertsPEM(authInfo.ClientCertificateData)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseCertsPEM() error = %v", err)
|
||||
}
|
||||
if got, want := certs[0].Subject.Organization, []string{clusterAdminsGroup}; !sameMembers(got, want) {
|
||||
t.Fatalf("client certificate organizations = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateJoinControlPlaneKubeConfigFilesForV129CreatesSuperAdmin(t *testing.T) {
|
||||
baseDir := t.TempDir()
|
||||
pkiDir := filepath.Join(baseDir, "pki")
|
||||
etcdDir := filepath.Join(pkiDir, "etcd")
|
||||
outDir := filepath.Join(baseDir, "etc")
|
||||
|
||||
if err := GenerateCertForKubeVersion(
|
||||
pkiDir,
|
||||
etcdDir,
|
||||
[]string{"10.96.0.1", "apiserver.test"},
|
||||
"192.168.0.10",
|
||||
"master0",
|
||||
"10.64.0.0/10",
|
||||
"cluster.local",
|
||||
kubeVersion129,
|
||||
); err != nil {
|
||||
t.Fatalf("GenerateCertForKubeVersion() error = %v", err)
|
||||
}
|
||||
|
||||
cfg := Config{
|
||||
Path: pkiDir,
|
||||
BaseName: "ca",
|
||||
}
|
||||
|
||||
if err := CreateJoinControlPlaneKubeConfigFilesForKubeVersion(
|
||||
outDir,
|
||||
cfg,
|
||||
"master0",
|
||||
"https://apiserver.test:6443",
|
||||
"kubernetes",
|
||||
kubeVersion129,
|
||||
); err != nil {
|
||||
t.Fatalf("CreateJoinControlPlaneKubeConfigFilesForKubeVersion() error = %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(outDir, superAdminKubeConfigFileName)); err != nil {
|
||||
t.Fatalf("expected %s to be created: %v", superAdminKubeConfigFileName, err)
|
||||
}
|
||||
|
||||
superAdminConfig, err := clientcmd.LoadFromFile(filepath.Join(outDir, superAdminKubeConfigFileName))
|
||||
if err != nil {
|
||||
t.Fatalf("LoadFromFile(super-admin.conf) error = %v", err)
|
||||
}
|
||||
authInfo := superAdminConfig.AuthInfos["kubernetes-super-admin"]
|
||||
if authInfo == nil {
|
||||
t.Fatalf("expected kubernetes-super-admin auth info to exist")
|
||||
}
|
||||
|
||||
certs, err := certutil.ParseCertsPEM(authInfo.ClientCertificateData)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseCertsPEM() error = %v", err)
|
||||
}
|
||||
if got, want := certs[0].Subject.Organization, []string{legacyPrivilegedGroup}; !sameMembers(got, want) {
|
||||
t.Fatalf("super-admin organizations = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateJoinControlPlaneKubeConfigFilesForPre129SkipsSuperAdmin(t *testing.T) {
|
||||
baseDir := t.TempDir()
|
||||
pkiDir := filepath.Join(baseDir, "pki")
|
||||
etcdDir := filepath.Join(pkiDir, "etcd")
|
||||
outDir := filepath.Join(baseDir, "etc")
|
||||
|
||||
if err := GenerateCert(
|
||||
pkiDir,
|
||||
etcdDir,
|
||||
[]string{"10.96.0.1", "apiserver.test"},
|
||||
"192.168.0.10",
|
||||
"master0",
|
||||
"10.64.0.0/10",
|
||||
"cluster.local",
|
||||
); err != nil {
|
||||
t.Fatalf("GenerateCert() error = %v", err)
|
||||
}
|
||||
|
||||
cfg := Config{
|
||||
Path: pkiDir,
|
||||
BaseName: "ca",
|
||||
}
|
||||
|
||||
if err := CreateJoinControlPlaneKubeConfigFilesForKubeVersion(
|
||||
outDir,
|
||||
cfg,
|
||||
"master0",
|
||||
"https://apiserver.test:6443",
|
||||
"kubernetes",
|
||||
"v1.28.9",
|
||||
); err != nil {
|
||||
t.Fatalf("CreateJoinControlPlaneKubeConfigFilesForKubeVersion() error = %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(outDir, superAdminKubeConfigFileName)); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected %s to be absent for pre-v1.29 configs, got err=%v", superAdminKubeConfigFileName, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenewAdminKubeConfigFileOverwritesExistingAdminConfig(t *testing.T) {
|
||||
baseDir := t.TempDir()
|
||||
pkiDir := filepath.Join(baseDir, "pki")
|
||||
etcdDir := filepath.Join(pkiDir, "etcd")
|
||||
outDir := filepath.Join(baseDir, "etc")
|
||||
|
||||
if err := GenerateCert(
|
||||
pkiDir,
|
||||
etcdDir,
|
||||
[]string{"10.96.0.1", "apiserver.test"},
|
||||
"192.168.0.10",
|
||||
"master0",
|
||||
"10.64.0.0/10",
|
||||
"cluster.local",
|
||||
); err != nil {
|
||||
t.Fatalf("GenerateCert() error = %v", err)
|
||||
}
|
||||
|
||||
cfg := Config{
|
||||
Path: pkiDir,
|
||||
BaseName: "ca",
|
||||
}
|
||||
|
||||
if err := CreateJoinControlPlaneKubeConfigFiles(
|
||||
outDir,
|
||||
cfg,
|
||||
"master0",
|
||||
"https://old-apiserver.test:6443",
|
||||
"kubernetes",
|
||||
); err != nil {
|
||||
t.Fatalf("CreateJoinControlPlaneKubeConfigFiles() error = %v", err)
|
||||
}
|
||||
|
||||
oldConfig, err := clientcmd.LoadFromFile(filepath.Join(outDir, adminKubeConfigFileName))
|
||||
if err != nil {
|
||||
t.Fatalf("LoadFromFile() before renew error = %v", err)
|
||||
}
|
||||
oldAuthInfo := oldConfig.AuthInfos["kubernetes-admin"]
|
||||
if oldAuthInfo == nil {
|
||||
t.Fatalf("expected kubernetes-admin auth info before renew")
|
||||
}
|
||||
|
||||
newEndpoint := "https://new-apiserver.test:6443"
|
||||
if err := RenewAdminKubeConfigFile(outDir, cfg, newEndpoint, "kubernetes", nil); err != nil {
|
||||
t.Fatalf("RenewAdminKubeConfigFile() error = %v", err)
|
||||
}
|
||||
|
||||
newConfig, err := clientcmd.LoadFromFile(filepath.Join(outDir, adminKubeConfigFileName))
|
||||
if err != nil {
|
||||
t.Fatalf("LoadFromFile() after renew error = %v", err)
|
||||
}
|
||||
newAuthInfo := newConfig.AuthInfos["kubernetes-admin"]
|
||||
if newAuthInfo == nil {
|
||||
t.Fatalf("expected kubernetes-admin auth info after renew")
|
||||
}
|
||||
|
||||
if string(oldAuthInfo.ClientCertificateData) == string(newAuthInfo.ClientCertificateData) {
|
||||
t.Fatalf("expected renewed admin kubeconfig to contain a different client certificate")
|
||||
}
|
||||
|
||||
currentCtx := newConfig.Contexts[newConfig.CurrentContext]
|
||||
if currentCtx == nil {
|
||||
t.Fatalf("expected current context %q to exist", newConfig.CurrentContext)
|
||||
}
|
||||
cluster := newConfig.Clusters[currentCtx.Cluster]
|
||||
if cluster == nil {
|
||||
t.Fatalf("expected cluster %q to exist", currentCtx.Cluster)
|
||||
}
|
||||
if got := cluster.Server; got != newEndpoint {
|
||||
t.Fatalf("cluster server = %q, want %q", got, newEndpoint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenewKubeConfigFilesOverwritesSelectedConfigs(t *testing.T) {
|
||||
baseDir := t.TempDir()
|
||||
pkiDir := filepath.Join(baseDir, "pki")
|
||||
etcdDir := filepath.Join(pkiDir, "etcd")
|
||||
outDir := filepath.Join(baseDir, "etc")
|
||||
|
||||
if err := GenerateCert(
|
||||
pkiDir,
|
||||
etcdDir,
|
||||
[]string{"10.96.0.1", "apiserver.test"},
|
||||
"192.168.0.10",
|
||||
"master0",
|
||||
"10.64.0.0/10",
|
||||
"cluster.local",
|
||||
); err != nil {
|
||||
t.Fatalf("GenerateCert() error = %v", err)
|
||||
}
|
||||
|
||||
cfg := Config{
|
||||
Path: pkiDir,
|
||||
BaseName: "ca",
|
||||
}
|
||||
|
||||
if err := CreateJoinControlPlaneKubeConfigFiles(
|
||||
outDir,
|
||||
cfg,
|
||||
"master0",
|
||||
"https://old-apiserver.test:6443",
|
||||
"kubernetes",
|
||||
); err != nil {
|
||||
t.Fatalf("CreateJoinControlPlaneKubeConfigFiles() error = %v", err)
|
||||
}
|
||||
|
||||
if err := RenewKubeConfigFiles(
|
||||
outDir,
|
||||
cfg,
|
||||
"master0",
|
||||
"https://new-apiserver.test:6443",
|
||||
"kubernetes",
|
||||
nil,
|
||||
controllerManagerKubeConfigFileName,
|
||||
schedulerKubeConfigFileName,
|
||||
); err != nil {
|
||||
t.Fatalf("RenewKubeConfigFiles() error = %v", err)
|
||||
}
|
||||
|
||||
for _, name := range []string{controllerManagerKubeConfigFileName, schedulerKubeConfigFileName} {
|
||||
kubeConfig, err := clientcmd.LoadFromFile(filepath.Join(outDir, name))
|
||||
if err != nil {
|
||||
t.Fatalf("LoadFromFile(%q) error = %v", name, err)
|
||||
}
|
||||
currentCtx := kubeConfig.Contexts[kubeConfig.CurrentContext]
|
||||
if currentCtx == nil {
|
||||
t.Fatalf("expected current context %q to exist for %s", kubeConfig.CurrentContext, name)
|
||||
}
|
||||
cluster := kubeConfig.Clusters[currentCtx.Cluster]
|
||||
if cluster == nil {
|
||||
t.Fatalf("expected cluster %q to exist for %s", currentCtx.Cluster, name)
|
||||
}
|
||||
if got := cluster.Server; got != "https://new-apiserver.test:6443" {
|
||||
t.Fatalf("%s cluster server = %q, want %q", name, got, "https://new-apiserver.test:6443")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenewAdminKubeConfigFileAllowsCustomOrganizations(t *testing.T) {
|
||||
baseDir := t.TempDir()
|
||||
pkiDir := filepath.Join(baseDir, "pki")
|
||||
etcdDir := filepath.Join(pkiDir, "etcd")
|
||||
outDir := filepath.Join(baseDir, "etc")
|
||||
|
||||
if err := GenerateCert(
|
||||
pkiDir,
|
||||
etcdDir,
|
||||
[]string{"10.96.0.1", "apiserver.test"},
|
||||
"192.168.0.10",
|
||||
"master0",
|
||||
"10.64.0.0/10",
|
||||
"cluster.local",
|
||||
); err != nil {
|
||||
t.Fatalf("GenerateCert() error = %v", err)
|
||||
}
|
||||
|
||||
cfg := Config{
|
||||
Path: pkiDir,
|
||||
BaseName: "ca",
|
||||
}
|
||||
|
||||
if err := RenewAdminKubeConfigFile(outDir, cfg, "https://apiserver.test:6443", "kubernetes", []string{"platform:devs", "platform:ops"}); err != nil {
|
||||
t.Fatalf("RenewAdminKubeConfigFile() error = %v", err)
|
||||
}
|
||||
|
||||
adminConfig, err := clientcmd.LoadFromFile(filepath.Join(outDir, adminKubeConfigFileName))
|
||||
if err != nil {
|
||||
t.Fatalf("LoadFromFile() error = %v", err)
|
||||
}
|
||||
authInfo := adminConfig.AuthInfos["kubernetes-admin"]
|
||||
if authInfo == nil {
|
||||
t.Fatalf("expected kubernetes-admin auth info to exist")
|
||||
}
|
||||
|
||||
certs, err := certutil.ParseCertsPEM(authInfo.ClientCertificateData)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseCertsPEM() error = %v", err)
|
||||
}
|
||||
if got, want := slices.Clone(certs[0].Subject.Organization), []string{"platform:devs", "platform:ops"}; !sameMembers(got, want) {
|
||||
t.Fatalf("client certificate organizations = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenewAdminKubeConfigFileAllowsEmptyOrganizations(t *testing.T) {
|
||||
baseDir := t.TempDir()
|
||||
pkiDir := filepath.Join(baseDir, "pki")
|
||||
etcdDir := filepath.Join(pkiDir, "etcd")
|
||||
outDir := filepath.Join(baseDir, "etc")
|
||||
|
||||
if err := GenerateCert(
|
||||
pkiDir,
|
||||
etcdDir,
|
||||
[]string{"10.96.0.1", "apiserver.test"},
|
||||
"192.168.0.10",
|
||||
"master0",
|
||||
"10.64.0.0/10",
|
||||
"cluster.local",
|
||||
); err != nil {
|
||||
t.Fatalf("GenerateCert() error = %v", err)
|
||||
}
|
||||
|
||||
cfg := Config{
|
||||
Path: pkiDir,
|
||||
BaseName: "ca",
|
||||
}
|
||||
|
||||
if err := RenewAdminKubeConfigFile(outDir, cfg, "https://apiserver.test:6443", "kubernetes", []string{}); err != nil {
|
||||
t.Fatalf("RenewAdminKubeConfigFile() error = %v", err)
|
||||
}
|
||||
|
||||
adminConfig, err := clientcmd.LoadFromFile(filepath.Join(outDir, adminKubeConfigFileName))
|
||||
if err != nil {
|
||||
t.Fatalf("LoadFromFile() error = %v", err)
|
||||
}
|
||||
authInfo := adminConfig.AuthInfos["kubernetes-admin"]
|
||||
if authInfo == nil {
|
||||
t.Fatalf("expected kubernetes-admin auth info to exist")
|
||||
}
|
||||
|
||||
certs, err := certutil.ParseCertsPEM(authInfo.ClientCertificateData)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseCertsPEM() error = %v", err)
|
||||
}
|
||||
if len(certs[0].Subject.Organization) != 0 {
|
||||
t.Fatalf("client certificate organizations = %v, want none", certs[0].Subject.Organization)
|
||||
}
|
||||
}
|
||||
|
||||
func sameMembers(got, want []string) bool {
|
||||
if len(got) != len(want) {
|
||||
return false
|
||||
}
|
||||
slices.Sort(got)
|
||||
want = slices.Clone(want)
|
||||
slices.Sort(want)
|
||||
return slices.Equal(got, want)
|
||||
}
|
||||
@@ -98,7 +98,7 @@ func (ke *kubeExpansion) UpdateKubeletConfig(ctx context.Context, kubeletConfig
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cm.Data[ckubeadm.KubeletBaseConfigurationConfigMap] = kubeletConfig
|
||||
cm.Data[ckubeadm.KubeletBaseConfigurationConfigMapKey] = kubeletConfig
|
||||
_, err = ke.client.CoreV1().ConfigMaps(metav1.NamespaceSystem).Update(ctx, cm, metav1.UpdateOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -20,7 +20,11 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
ckubeadm "k8s.io/kubernetes/cmd/kubeadm/app/constants"
|
||||
)
|
||||
|
||||
func TestGetKubeadmConfig(t *testing.T) {
|
||||
@@ -53,3 +57,32 @@ func TestGetKubeadmConfig(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateKubeletConfigUsesConfigMapDataKey(t *testing.T) {
|
||||
client := fake.NewSimpleClientset(&v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: ckubeadm.KubeletBaseConfigurationConfigMap,
|
||||
Namespace: metav1.NamespaceSystem,
|
||||
},
|
||||
Data: map[string]string{
|
||||
ckubeadm.KubeletBaseConfigurationConfigMapKey: "old-config",
|
||||
},
|
||||
})
|
||||
|
||||
const want = "new-config"
|
||||
ke := NewKubeExpansion(client)
|
||||
if err := ke.UpdateKubeletConfig(context.Background(), want); err != nil {
|
||||
t.Fatalf("UpdateKubeletConfig() error = %v", err)
|
||||
}
|
||||
|
||||
cm, err := client.CoreV1().ConfigMaps(metav1.NamespaceSystem).Get(context.Background(), ckubeadm.KubeletBaseConfigurationConfigMap, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("get kubelet configmap: %v", err)
|
||||
}
|
||||
if got := cm.Data[ckubeadm.KubeletBaseConfigurationConfigMapKey]; got != want {
|
||||
t.Fatalf("kubelet config key = %q, want %q", got, want)
|
||||
}
|
||||
if _, ok := cm.Data[ckubeadm.KubeletBaseConfigurationConfigMap]; ok {
|
||||
t.Fatalf("unexpected data under configmap name key %q", ckubeadm.KubeletBaseConfigurationConfigMap)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import (
|
||||
const (
|
||||
localhost = "127.0.0.1"
|
||||
defaultTemporaryPort = "5050"
|
||||
registryPIDFileName = "registry.pid"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -70,21 +71,6 @@ func (s *impl) Sync(ctx context.Context, hosts ...string) error {
|
||||
return nil
|
||||
}
|
||||
logger.Info("trying default http mode to sync images to hosts %v", hosts)
|
||||
// run `sealctl registry serve` to start a temporary registry
|
||||
for i := range hosts {
|
||||
cmdCtx, cancel := context.WithCancel(ctx)
|
||||
// defer cancel async commands
|
||||
defer cancel()
|
||||
go func(ctx context.Context, host string) {
|
||||
logger.Debug("running temporary registry on host %s", host)
|
||||
if err := s.execer.CmdAsyncWithContext(ctx, host, getRegistryServeCommand(s.pathResolver, defaultTemporaryPort)); err != nil {
|
||||
// ignore expected signal killed error when context cancel
|
||||
if !strings.Contains(err.Error(), "signal: killed") && !strings.Contains(err.Error(), "context canceled") {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}(cmdCtx, hosts[i])
|
||||
}
|
||||
|
||||
type syncOption struct {
|
||||
target string
|
||||
@@ -92,22 +78,67 @@ func (s *impl) Sync(ctx context.Context, hosts ...string) error {
|
||||
}
|
||||
|
||||
syncOptionChan := make(chan *syncOption, len(hosts))
|
||||
go func() {
|
||||
for i := range hosts {
|
||||
go func(target string) {
|
||||
probeCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer cancel()
|
||||
ep := sync.ParseRegistryAddress(trimPortStr(target), defaultTemporaryPort)
|
||||
if err := httputils.WaitUntilEndpointAlive(probeCtx, "http://"+ep); err != nil {
|
||||
logger.Warn("cannot connect to remote temporary registry %s: %v, fallback using ssh mode instead", ep, err)
|
||||
syncOptionChan <- &syncOption{target: target, typ: sshMode}
|
||||
} else {
|
||||
syncOptionChan <- &syncOption{target: ep, typ: httpMode}
|
||||
}
|
||||
}(hosts[i])
|
||||
var (
|
||||
cancelFuncs []context.CancelFunc
|
||||
temporaryRegistries []string
|
||||
)
|
||||
defer func() {
|
||||
for _, cancel := range cancelFuncs {
|
||||
cancel()
|
||||
}
|
||||
for _, host := range temporaryRegistries {
|
||||
if err := s.cleanupRemoteTemporaryRegistry(host); err != nil {
|
||||
logger.Debug("failed to cleanup remote temporary registry on host %s: %v", host, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// run `sealctl registry serve` to start a temporary registry
|
||||
for i := range hosts {
|
||||
ep := sync.ParseRegistryAddress(trimPortStr(hosts[i]), defaultTemporaryPort)
|
||||
if err := checkRemoteTemporaryRegistry(ctx, hosts[i], defaultTemporaryPort); err == nil {
|
||||
logger.Debug("remote temporary registry %s is already alive, reuse it", ep)
|
||||
syncOptionChan <- &syncOption{target: ep, typ: httpMode}
|
||||
continue
|
||||
}
|
||||
|
||||
if err := s.cleanupRemoteTemporaryRegistry(hosts[i]); err != nil {
|
||||
logger.Debug("failed to cleanup stale remote temporary registry on host %s: %v", hosts[i], err)
|
||||
}
|
||||
|
||||
registryServeCommand, ok := s.getRegistryServeCommand(hosts[i], defaultTemporaryPort)
|
||||
if !ok {
|
||||
logger.Debug("remote temporary registry is unsupported on host %s, fallback using ssh mode instead", hosts[i])
|
||||
syncOptionChan <- &syncOption{target: hosts[i], typ: sshMode}
|
||||
continue
|
||||
}
|
||||
|
||||
cmdCtx, cancel := context.WithCancel(ctx)
|
||||
cancelFuncs = append(cancelFuncs, cancel)
|
||||
temporaryRegistries = append(temporaryRegistries, hosts[i])
|
||||
go func(ctx context.Context, host, registryServeCommand string) {
|
||||
logger.Debug("running temporary registry on host %s", host)
|
||||
if err := s.execer.CmdAsyncWithContext(ctx, host, registryServeCommand); err != nil {
|
||||
// ignore expected signal killed error when context cancel
|
||||
if !strings.Contains(err.Error(), "signal: killed") && !strings.Contains(err.Error(), "context canceled") {
|
||||
if waitRemoteTemporaryRegistry(ctx, host, defaultTemporaryPort) == nil {
|
||||
logger.Debug("remote temporary registry on host %s is already alive, ignore serve command error: %v", host, err)
|
||||
return
|
||||
}
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}(cmdCtx, hosts[i], registryServeCommand)
|
||||
go func(target, ep string) {
|
||||
if err := waitRemoteTemporaryRegistry(ctx, target, defaultTemporaryPort); err != nil {
|
||||
logger.Warn("cannot connect to remote temporary registry %s: %v, fallback using ssh mode instead", ep, err)
|
||||
syncOptionChan <- &syncOption{target: target, typ: sshMode}
|
||||
} else {
|
||||
syncOptionChan <- &syncOption{target: ep, typ: httpMode}
|
||||
}
|
||||
}(hosts[i], ep)
|
||||
}
|
||||
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
for i := 0; i < len(hosts); i++ {
|
||||
opt, ok := <-syncOptionChan
|
||||
@@ -133,6 +164,29 @@ func (s *impl) Sync(ctx context.Context, hosts ...string) error {
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func (s *impl) cleanupRemoteTemporaryRegistry(host string) error {
|
||||
output, err := s.execer.Cmd(host, getRegistryServeCleanupCommand(s.pathResolver))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %s", err, strings.TrimSpace(string(output)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func waitRemoteTemporaryRegistry(ctx context.Context, target, port string) error {
|
||||
return waitRemoteTemporaryRegistryWithTimeout(ctx, target, port, 3*time.Second)
|
||||
}
|
||||
|
||||
func checkRemoteTemporaryRegistry(ctx context.Context, target, port string) error {
|
||||
return waitRemoteTemporaryRegistryWithTimeout(ctx, target, port, 500*time.Millisecond)
|
||||
}
|
||||
|
||||
func waitRemoteTemporaryRegistryWithTimeout(ctx context.Context, target, port string, timeout time.Duration) error {
|
||||
probeCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
ep := sync.ParseRegistryAddress(trimPortStr(target), port)
|
||||
return httputils.WaitUntilEndpointAlive(probeCtx, "http://"+ep)
|
||||
}
|
||||
|
||||
func trimPortStr(s string) string {
|
||||
if idx := strings.Index(s, ":"); idx > 0 {
|
||||
return s[:idx]
|
||||
@@ -140,12 +194,74 @@ func trimPortStr(s string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
func getRegistryServeCommand(pathResolver constants.PathResolver, port string) string {
|
||||
return fmt.Sprintf("%s registry serve filesystem -p %s --disable-logging=true %s",
|
||||
pathResolver.RootFSSealctlPath(), port, pathResolver.RootFSRegistryPath(),
|
||||
type registryServeFlags struct {
|
||||
portFlag string
|
||||
pidFile bool
|
||||
disableLogging bool
|
||||
}
|
||||
|
||||
func (s *impl) getRegistryServeCommand(host, port string) (string, bool) {
|
||||
output, err := s.execer.Cmd(host, getRegistryServeProbeCommand(s.pathResolver, port))
|
||||
if err != nil {
|
||||
logger.Debug("failed to probe remote temporary registry on host %s: %v", host, err)
|
||||
return "", false
|
||||
}
|
||||
|
||||
fields := strings.Fields(strings.TrimSpace(string(output)))
|
||||
if len(fields) == 0 {
|
||||
return "", false
|
||||
}
|
||||
flags := registryServeFlags{portFlag: fields[0]}
|
||||
switch flags.portFlag {
|
||||
case "--port", "-p":
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
for _, field := range fields[1:] {
|
||||
switch field {
|
||||
case "--pid-file":
|
||||
flags.pidFile = true
|
||||
case "--disable-logging":
|
||||
flags.disableLogging = true
|
||||
}
|
||||
}
|
||||
return getRegistryServeCommand(s.pathResolver, port, flags), true
|
||||
}
|
||||
|
||||
func getRegistryServeProbeCommand(pathResolver constants.PathResolver, port string) string {
|
||||
return fmt.Sprintf("if %[1]s registry serve filesystem --port %[2]s --help >/dev/null 2>&1; then printf -- '--port'; elif %[1]s registry serve filesystem -p %[2]s --help >/dev/null 2>&1; then printf -- '-p'; else exit 0; fi; if %[1]s registry serve filesystem --pid-file %[3]s --help >/dev/null 2>&1; then printf ' --pid-file'; fi; if %[1]s registry serve filesystem --disable-logging=true --help >/dev/null 2>&1; then printf ' --disable-logging'; fi",
|
||||
pathResolver.RootFSSealctlPath(), port, registryPIDFile(pathResolver),
|
||||
)
|
||||
}
|
||||
|
||||
func getRegistryServeCommand(pathResolver constants.PathResolver, port string, flags registryServeFlags) string {
|
||||
args := []string{
|
||||
pathResolver.RootFSSealctlPath(),
|
||||
"registry", "serve", "filesystem",
|
||||
flags.portFlag, port,
|
||||
}
|
||||
if flags.pidFile {
|
||||
args = append(args, "--pid-file", registryPIDFile(pathResolver))
|
||||
}
|
||||
if flags.disableLogging {
|
||||
args = append(args, "--disable-logging=true")
|
||||
}
|
||||
args = append(args, pathResolver.RootFSRegistryPath())
|
||||
return strings.Join(args, " ") + " >/dev/null 2>&1"
|
||||
}
|
||||
|
||||
func getRegistryServeCleanupCommand(pathResolver constants.PathResolver) string {
|
||||
return fmt.Sprintf("if [ -s %[1]s ]; then pid=$(cat %[1]s 2>/dev/null || true); if [ -n \"$pid\" ] && [ -r \"/proc/$pid/cmdline\" ]; then cmd=$(tr '\\000' ' ' < \"/proc/$pid/cmdline\" 2>/dev/null || true); case \"$cmd\" in *\"%[2]s registry serve filesystem\"*\"%[3]s\"*) kill \"$pid\" 2>/dev/null || true;; esac; fi; rm -f %[1]s; fi",
|
||||
registryPIDFile(pathResolver),
|
||||
pathResolver.RootFSSealctlPath(),
|
||||
pathResolver.RootFSRegistryPath(),
|
||||
)
|
||||
}
|
||||
|
||||
func registryPIDFile(pathResolver constants.PathResolver) string {
|
||||
return filepath.Join(pathResolver.RootFSPath(), registryPIDFileName)
|
||||
}
|
||||
|
||||
func syncViaSSH(_ context.Context, s *impl, target string, localDir string) error {
|
||||
return ssh.CopyDir(s.execer, target, localDir, s.pathResolver.RootFSRegistryPath(), nil)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
Copyright 2026 sealos.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package registry
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type testPathResolver struct{}
|
||||
|
||||
func (testPathResolver) Root() string { return "/var/lib/sealos/data/default" }
|
||||
func (testPathResolver) RootFSPath() string { return "/var/lib/sealos/data/default/rootfs" }
|
||||
func (testPathResolver) RootFSEtcPath() string { return "/var/lib/sealos/data/default/rootfs/etc" }
|
||||
func (testPathResolver) RootFSStaticsPath() string {
|
||||
return "/var/lib/sealos/data/default/rootfs/statics"
|
||||
}
|
||||
func (testPathResolver) RootFSScriptsPath() string {
|
||||
return "/var/lib/sealos/data/default/rootfs/scripts"
|
||||
}
|
||||
func (testPathResolver) RootFSRegistryPath() string {
|
||||
return "/var/lib/sealos/data/default/rootfs/registry"
|
||||
}
|
||||
func (testPathResolver) RootFSManifestsPath() string {
|
||||
return "/var/lib/sealos/data/default/rootfs/manifests"
|
||||
}
|
||||
func (testPathResolver) RootFSBinPath() string { return "/var/lib/sealos/data/default/rootfs/bin" }
|
||||
func (testPathResolver) RootFSSealctlPath() string {
|
||||
return "/var/lib/sealos/data/default/rootfs/opt/sealctl"
|
||||
}
|
||||
func (testPathResolver) ConfigsPath() string { return "/var/lib/sealos/data/default/etc" }
|
||||
func (testPathResolver) RunRoot() string { return "/var/lib/sealos/default" }
|
||||
func (testPathResolver) PkiPath() string { return "/var/lib/sealos/default/pki" }
|
||||
func (testPathResolver) PkiEtcdPath() string { return "/var/lib/sealos/default/pki/etcd" }
|
||||
func (testPathResolver) AdminFile() string { return "/var/lib/sealos/default/admin.conf" }
|
||||
func (testPathResolver) EtcPath() string { return "/var/lib/sealos/default/etc" }
|
||||
func (testPathResolver) TmpPath() string { return "/var/lib/sealos/default/tmp" }
|
||||
|
||||
func TestGetRegistryServeCommandIncludesSupportedFlags(t *testing.T) {
|
||||
got := getRegistryServeCommand(testPathResolver{}, "5050", registryServeFlags{
|
||||
portFlag: "--port",
|
||||
pidFile: true,
|
||||
disableLogging: true,
|
||||
})
|
||||
want := "/var/lib/sealos/data/default/rootfs/opt/sealctl registry serve filesystem --port 5050 --pid-file /var/lib/sealos/data/default/rootfs/registry.pid --disable-logging=true /var/lib/sealos/data/default/rootfs/registry >/dev/null 2>&1"
|
||||
if got != want {
|
||||
t.Fatalf("unexpected serve command:\nwant: %s\n got: %s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRegistryServeCommandOmitsUnsupportedOptionalFlags(t *testing.T) {
|
||||
got := getRegistryServeCommand(testPathResolver{}, "5050", registryServeFlags{portFlag: "-p"})
|
||||
want := "/var/lib/sealos/data/default/rootfs/opt/sealctl registry serve filesystem -p 5050 /var/lib/sealos/data/default/rootfs/registry >/dev/null 2>&1"
|
||||
if got != want {
|
||||
t.Fatalf("unexpected serve command:\nwant: %s\n got: %s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRegistryServeCleanupCommandIsScopedToTemporaryRegistry(t *testing.T) {
|
||||
got := getRegistryServeCleanupCommand(testPathResolver{})
|
||||
for _, want := range []string{
|
||||
"/var/lib/sealos/data/default/rootfs/registry.pid",
|
||||
"/proc/$pid/cmdline",
|
||||
"/var/lib/sealos/data/default/rootfs/opt/sealctl registry serve filesystem",
|
||||
"/var/lib/sealos/data/default/rootfs/registry",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("cleanup command %q does not contain %q", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
osexec "os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
@@ -34,6 +37,7 @@ import (
|
||||
v2 "github.com/labring/sealos/pkg/types/v1beta1"
|
||||
executils "github.com/labring/sealos/pkg/utils/exec"
|
||||
"github.com/labring/sealos/pkg/utils/file"
|
||||
"github.com/labring/sealos/pkg/utils/hash"
|
||||
"github.com/labring/sealos/pkg/utils/logger"
|
||||
"github.com/labring/sealos/pkg/utils/maps"
|
||||
stringsutil "github.com/labring/sealos/pkg/utils/strings"
|
||||
@@ -113,6 +117,7 @@ func (f *defaultRootfs) mountRootfs(cluster *v2.Cluster, ipList []string) error
|
||||
return errors.New("cannot mount a cluster without rootfs, this is an unexpected bug")
|
||||
}
|
||||
rootfsEnvs := v2.MergeEnvWithBuiltinKeys(rootfs.Env, *rootfs)
|
||||
localSealctlPath := findLocalSealctl()
|
||||
|
||||
for idx := range ipList {
|
||||
ip := ipList[idx]
|
||||
@@ -133,6 +138,9 @@ func (f *defaultRootfs) mountRootfs(cluster *v2.Cluster, ipList []string) error
|
||||
envs := envProcessor.Getenv(ip)
|
||||
envs = maps.Merge(rootfsEnvs, envs)
|
||||
envs[v2.ImageRunModeEnvSysKey] = strings.Join(cluster.GetRolesByIP(ip), ",")
|
||||
if err := syncSealctlToRootfs(execer, ip, localSealctlPath, pathResolver.RootFSSealctlPath()); err != nil {
|
||||
logger.Warn("failed to sync sealctl to %s: %v", ip, err)
|
||||
}
|
||||
renderCommand := getRenderCommand(pathResolver.RootFSSealctlPath(), target)
|
||||
|
||||
return execer.CmdAsync(ip, stringsutil.RenderShellWithEnv(renderCommand, envs))
|
||||
@@ -152,6 +160,9 @@ func (f *defaultRootfs) mountRootfs(cluster *v2.Cluster, ipList []string) error
|
||||
if err := copyFn(mountInfo, master0, targetDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := syncSealctlToRootfs(execer, master0, localSealctlPath, pathResolver.RootFSSealctlPath()); err != nil {
|
||||
logger.Warn("failed to sync sealctl to %s: %v", master0, err)
|
||||
}
|
||||
renderCommand := getRenderCommand(pathResolver.RootFSSealctlPath(), targetDir)
|
||||
return execer.CmdAsync(master0, stringsutil.RenderShellWithEnv(renderCommand, mountInfo.Env))
|
||||
}
|
||||
@@ -163,7 +174,7 @@ func (f *defaultRootfs) mountRootfs(cluster *v2.Cluster, ipList []string) error
|
||||
|
||||
func getRenderCommand(binary string, target string) string {
|
||||
// skip if sealctl doesn't has subcommand render
|
||||
return fmt.Sprintf("%s render --debug=%v --clear %s 2>/dev/null || true", binary,
|
||||
return fmt.Sprintf("%s render --debug=%v --clear %s >/dev/null 2>&1 || true", binary,
|
||||
logger.IsDebugMode(),
|
||||
strings.Join([]string{
|
||||
filepath.Join(target, constants.EtcDirName),
|
||||
@@ -172,6 +183,98 @@ func getRenderCommand(binary string, target string) string {
|
||||
}, " "))
|
||||
}
|
||||
|
||||
func findLocalSealctl() string {
|
||||
if runtime.GOOS != "linux" {
|
||||
return ""
|
||||
}
|
||||
|
||||
var candidates []string
|
||||
if path := os.Getenv("SEALOS_SEALCTL_PATH"); path != "" {
|
||||
candidates = append(candidates, path)
|
||||
}
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
candidates = append(candidates, filepath.Join(filepath.Dir(exe), "sealctl"))
|
||||
}
|
||||
if path, err := osexec.LookPath("sealctl"); err == nil {
|
||||
candidates = append(candidates, path)
|
||||
}
|
||||
candidates = append(candidates, "/tmp/sealctl")
|
||||
|
||||
for _, candidate := range candidates {
|
||||
if isExecutableFile(candidate) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isExecutableFile(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
return err == nil && !info.IsDir() && info.Mode()&0111 != 0
|
||||
}
|
||||
|
||||
func syncSealctlToRootfs(execer exec.Interface, host, localSealctlPath, remoteSealctlPath string) error {
|
||||
if localSealctlPath == "" {
|
||||
return nil
|
||||
}
|
||||
if !isRemoteArchCompatible(execer, host) {
|
||||
return nil
|
||||
}
|
||||
if isRemoteSealctlCurrent(execer, host, localSealctlPath, remoteSealctlPath) {
|
||||
return nil
|
||||
}
|
||||
|
||||
tmpPath := remoteSealctlPath + ".new"
|
||||
if err := execer.Copy(host, localSealctlPath, tmpPath); err != nil {
|
||||
return err
|
||||
}
|
||||
return execer.CmdAsync(host, fmt.Sprintf("install -m 0755 %s %s && rm -f %s",
|
||||
shellQuote(tmpPath),
|
||||
shellQuote(remoteSealctlPath),
|
||||
shellQuote(tmpPath),
|
||||
))
|
||||
}
|
||||
|
||||
func isRemoteSealctlCurrent(execer exec.Interface, host, localSealctlPath, remoteSealctlPath string) bool {
|
||||
localDigest := hash.FileDigest(localSealctlPath)
|
||||
if localDigest == "" {
|
||||
return false
|
||||
}
|
||||
output, err := execer.Cmd(host, fmt.Sprintf("if [ -f %s ]; then sha256sum %s | cut -d' ' -f1; fi",
|
||||
shellQuote(remoteSealctlPath),
|
||||
shellQuote(remoteSealctlPath),
|
||||
))
|
||||
if err != nil {
|
||||
logger.Debug("failed to get remote sealctl digest on host %s: %v", host, err)
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(string(output)) == localDigest
|
||||
}
|
||||
|
||||
func isRemoteArchCompatible(execer exec.Interface, host string) bool {
|
||||
output, err := execer.Cmd(host, "uname -m")
|
||||
if err != nil {
|
||||
logger.Debug("failed to detect remote arch on host %s: %v", host, err)
|
||||
return false
|
||||
}
|
||||
return normalizeArch(strings.TrimSpace(string(output))) == runtime.GOARCH
|
||||
}
|
||||
|
||||
func normalizeArch(arch string) string {
|
||||
switch arch {
|
||||
case "x86_64", "amd64":
|
||||
return "amd64"
|
||||
case "aarch64", "arm64":
|
||||
return "arm64"
|
||||
default:
|
||||
return arch
|
||||
}
|
||||
}
|
||||
|
||||
func shellQuote(value string) string {
|
||||
return "'" + strings.ReplaceAll(value, "'", "'\"'\"'") + "'"
|
||||
}
|
||||
|
||||
func (f *defaultRootfs) unmountRootfs(cluster *v2.Cluster, ipList []string) error {
|
||||
clusterRootfsDir := constants.NewPathResolver(cluster.Name).Root()
|
||||
rmRootfs := fmt.Sprintf("rm -rf %s", clusterRootfsDir)
|
||||
|
||||
@@ -17,6 +17,7 @@ package helpers
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/labring/image-cri-shim/pkg/types"
|
||||
|
||||
@@ -66,6 +67,21 @@ func GetRegistryInfo(execer exec.Interface, rootfs, defaultRegistry string) *v1b
|
||||
return readConfig
|
||||
}
|
||||
|
||||
// WaitRegistryReady waits until the registry endpoint is reachable from host.
|
||||
func WaitRegistryReady(execer exec.Interface, host, domain, port string) error {
|
||||
registryAddr := fmt.Sprintf("%s:%s", domain, port)
|
||||
logger.Info("waiting for registry %s to be ready on %s", registryAddr, host)
|
||||
cmd := fmt.Sprintf(`registry_host=%s; registry_port=%s; if ! command -v bash >/dev/null 2>&1 || ! command -v timeout >/dev/null 2>&1; then exit 1; fi; i=0; while [ "$i" -lt 120 ]; do if timeout 1 bash -c '</dev/tcp/'"$registry_host"'/'"$registry_port" >/dev/null 2>&1; then exit 0; fi; i=$((i + 1)); sleep 1; done; exit 1`, shellQuote(domain), shellQuote(port))
|
||||
if err := execer.CmdAsync(host, cmd); err != nil {
|
||||
return fmt.Errorf("registry %s is not ready on %s: %w", registryAddr, host, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func shellQuote(s string) string {
|
||||
return "'" + strings.ReplaceAll(s, "'", `'"'"'`) + "'"
|
||||
}
|
||||
|
||||
func GetImageCRIShimInfo(execer exec.Interface, config, defaultIP string) *types.Config {
|
||||
out, _ := execer.Cmd(defaultIP, fmt.Sprintf("cat %s", config))
|
||||
logger.Debug("image shim data info: %s", string(out))
|
||||
|
||||
@@ -28,8 +28,13 @@ type Ruler interface {
|
||||
SyncNodeIPVS(masters, nodes []string) error
|
||||
}
|
||||
|
||||
type CertRenewOptions struct {
|
||||
Targets []string
|
||||
Groups []string
|
||||
}
|
||||
|
||||
type CertManager interface {
|
||||
Renew() error
|
||||
Renew(opts CertRenewOptions) error
|
||||
UpdateCertSANs(certSANs []string) error
|
||||
}
|
||||
|
||||
|
||||
@@ -18,11 +18,17 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
"github.com/labring/sealos/pkg/cert"
|
||||
"github.com/labring/sealos/pkg/runtime"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
|
||||
"github.com/labring/sealos/pkg/client-go/kubernetes"
|
||||
"github.com/labring/sealos/pkg/utils/file"
|
||||
@@ -32,13 +38,304 @@ import (
|
||||
|
||||
const (
|
||||
AdminConf = "admin.conf"
|
||||
SuperAdminConf = "super-admin.conf"
|
||||
ControllerConf = "controller-manager.conf"
|
||||
SchedulerConf = "scheduler.conf"
|
||||
KubeletConf = "kubelet.conf"
|
||||
)
|
||||
|
||||
func (k *KubeadmRuntime) Renew() error {
|
||||
return errors.New("not implement")
|
||||
func (k *KubeadmRuntime) localKubeVersion() string {
|
||||
if version := k.getKubeVersion(); version != "" {
|
||||
return version
|
||||
}
|
||||
return k.getKubeVersionFromImage()
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) Renew(opts runtime.CertRenewOptions) error {
|
||||
normalizedTargets, renewAll, err := normalizeRenewTargets(opts.Targets)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = validateRenewGroups(normalizedTargets, renewAll, opts.Groups); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if renewAll {
|
||||
return k.renewAllLocalCertMaterials(opts.Groups)
|
||||
}
|
||||
|
||||
hostName, err := k.execHostname(k.getMaster0IPAndPort())
|
||||
if err != nil {
|
||||
return fmt.Errorf("get hostname failed %v", err)
|
||||
}
|
||||
localTargets := effectiveLocalKubeConfigRenewTargets(normalizedTargets, k.localKubeVersion())
|
||||
if err = renewLocalKubeConfigFiles(k, hostName, localTargets, opts.Groups); err != nil {
|
||||
return err
|
||||
}
|
||||
if shouldEnsureAdminClusterRoleBinding(k.localKubeVersion(), opts.Groups, false, localTargets) {
|
||||
if err = k.ensureAdminClusterRoleBinding(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if containsRenewTarget(normalizedTargets, AdminConf) {
|
||||
if err := k.syncLocalAdminKubeConfigCopies(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
k.cli = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func containsRenewTarget(targets []string, want string) bool {
|
||||
for _, target := range targets {
|
||||
if target == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func effectiveLocalKubeConfigRenewTargets(targets []string, kubeVersion string) []string {
|
||||
effectiveTargets := append([]string{}, targets...)
|
||||
if containsRenewTarget(targets, AdminConf) && certUsesClusterAdminsIdentityModel(kubeVersion) && !containsRenewTarget(targets, SuperAdminConf) {
|
||||
effectiveTargets = append(effectiveTargets, SuperAdminConf)
|
||||
}
|
||||
return effectiveTargets
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) renewAllLocalCertMaterials(adminOrganizations []string) error {
|
||||
if err := k.mergeWithBuiltinKubeadmConfig(); err != nil {
|
||||
if strings.TrimSpace(k.getServiceCIDR()) == "" {
|
||||
return fmt.Errorf("load cluster networking and certSANs for local cert renew: %w", err)
|
||||
}
|
||||
logger.Warn("failed to refresh kubeadm config for local cert renew, using local config: %s", err.Error())
|
||||
}
|
||||
|
||||
hostName, err := k.execHostname(k.getMaster0IPAndPort())
|
||||
if err != nil {
|
||||
return fmt.Errorf("get hostname failed %v", err)
|
||||
}
|
||||
if err := cert.RenewLeafCertsForKubeVersion(
|
||||
k.pathResolver.PkiPath(),
|
||||
k.pathResolver.PkiEtcdPath(),
|
||||
k.getCertSANs(),
|
||||
k.getMaster0IP(),
|
||||
hostName,
|
||||
k.getServiceCIDR(),
|
||||
k.getDNSDomain(),
|
||||
k.localKubeVersion(),
|
||||
); err != nil {
|
||||
return fmt.Errorf("failed to renew local pki files: %w", err)
|
||||
}
|
||||
localTargets := defaultLocalKubeConfigFiles(k.localKubeVersion())
|
||||
if err := renewLocalKubeConfigFiles(k, hostName, localTargets, adminOrganizations); err != nil {
|
||||
return err
|
||||
}
|
||||
if shouldEnsureAdminClusterRoleBinding(k.localKubeVersion(), adminOrganizations, true, localTargets) {
|
||||
if err := k.ensureAdminClusterRoleBinding(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := k.syncLocalAdminKubeConfigCopies(); err != nil {
|
||||
return err
|
||||
}
|
||||
k.cli = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func renewLocalKubeConfigFiles(k *KubeadmRuntime, hostName string, files []string, adminOrganizations []string) error {
|
||||
return renewLocalKubeConfigFilesForVersion(k, hostName, k.localKubeVersion(), files, adminOrganizations)
|
||||
}
|
||||
|
||||
func renewLocalKubeConfigFilesForVersion(k *KubeadmRuntime, hostName, kubeVersion string, files []string, adminOrganizations []string) error {
|
||||
certConfig := cert.Config{
|
||||
Path: k.pathResolver.PkiPath(),
|
||||
BaseName: "ca",
|
||||
}
|
||||
if err := cert.RenewKubeConfigFilesForKubeVersion(
|
||||
k.pathResolver.EtcPath(),
|
||||
certConfig,
|
||||
hostName,
|
||||
k.getClusterAPIServer(),
|
||||
"kubernetes",
|
||||
adminOrganizations,
|
||||
kubeVersion,
|
||||
files...,
|
||||
); err != nil {
|
||||
return fmt.Errorf("failed to renew local kubeconfig files %v: %w", files, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func defaultLocalKubeConfigFiles(kubeVersion string) []string {
|
||||
files := []string{AdminConf, ControllerConf, SchedulerConf, KubeletConf}
|
||||
if certUsesClusterAdminsIdentityModel(kubeVersion) {
|
||||
files = append(files, SuperAdminConf)
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
func remoteControlPlaneKubeConfigFiles(includeKubelet bool) []string {
|
||||
// super-admin.conf is intentionally kept local to the sealos host.
|
||||
files := []string{AdminConf, ControllerConf, SchedulerConf}
|
||||
if includeKubelet {
|
||||
files = append(files, KubeletConf)
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
func certUsesClusterAdminsIdentityModel(kubeVersion string) bool {
|
||||
return cert.UsesClusterAdminsIdentityModel(kubeVersion)
|
||||
}
|
||||
|
||||
func shouldEnsureAdminClusterRoleBinding(kubeVersion string, adminOrganizations []string, renewAll bool, targets []string) bool {
|
||||
if !certUsesClusterAdminsIdentityModel(kubeVersion) || adminOrganizations != nil {
|
||||
return false
|
||||
}
|
||||
if renewAll {
|
||||
return true
|
||||
}
|
||||
for _, target := range targets {
|
||||
if target == AdminConf {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) fetchMasterKubeConfigFile(name string) (string, error) {
|
||||
localPath := filepath.Join(k.pathResolver.EtcPath(), name)
|
||||
if err := os.MkdirAll(k.pathResolver.EtcPath(), 0o755); err != nil {
|
||||
return "", fmt.Errorf("create local etc dir for %s: %w", name, err)
|
||||
}
|
||||
remotePath := path.Join(kubernetesEtc, name)
|
||||
if err := k.sshFetch(k.getMaster0IPAndPort(), remotePath, localPath); err != nil {
|
||||
return "", fmt.Errorf("fetch %s from master0: %w", remotePath, err)
|
||||
}
|
||||
return localPath, nil
|
||||
}
|
||||
|
||||
// resolvePrivilegedKubeConfig returns a kubeconfig that still has cluster-admin
|
||||
// privileges on the live cluster. Prefer master0 copies over sealos-managed local
|
||||
// files, which may be signed by stale PKI from a previous partial upgrade.
|
||||
func (k *KubeadmRuntime) resolvePrivilegedKubeConfig() (string, error) {
|
||||
if kubeconfig, err := k.fetchMasterKubeConfigFile(SuperAdminConf); err == nil {
|
||||
return kubeconfig, nil
|
||||
}
|
||||
if kubeconfig, err := k.fetchMasterKubeConfigFile(AdminConf); err == nil {
|
||||
return kubeconfig, nil
|
||||
}
|
||||
localSuperAdmin := filepath.Join(k.pathResolver.EtcPath(), SuperAdminConf)
|
||||
if file.IsExist(localSuperAdmin) {
|
||||
return localSuperAdmin, nil
|
||||
}
|
||||
return k.pathResolver.AdminFile(), nil
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) resolveSuperAdminKubeConfig() (string, error) {
|
||||
return k.resolvePrivilegedKubeConfig()
|
||||
}
|
||||
|
||||
func isRetryableKubernetesClientError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
msg := err.Error()
|
||||
return strings.Contains(msg, "connection refused") ||
|
||||
strings.Contains(msg, "no route to host") ||
|
||||
strings.Contains(msg, "i/o timeout") ||
|
||||
strings.Contains(msg, "TLS handshake timeout") ||
|
||||
strings.Contains(msg, "EOF")
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) ensureAdminClusterRoleBinding() error {
|
||||
kubeconfig, err := k.resolvePrivilegedKubeConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiserver := k.getClusterAPIServer()
|
||||
|
||||
clusterRoleBinding := &rbacv1.ClusterRoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "kubeadm:cluster-admins",
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: rbacv1.GroupName,
|
||||
Kind: "ClusterRole",
|
||||
Name: "cluster-admin",
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: rbacv1.GroupKind,
|
||||
Name: "kubeadm:cluster-admins",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(2 * time.Minute)
|
||||
var lastErr error
|
||||
for {
|
||||
client, err := kubernetes.NewKubernetesClient(kubeconfig, apiserver)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build kubernetes client from %s: %w", kubeconfig, err)
|
||||
}
|
||||
lastErr = kubernetes.NewKubeIdempotency(client.Kubernetes()).CreateOrUpdateClusterRoleBinding(clusterRoleBinding)
|
||||
if lastErr == nil {
|
||||
return nil
|
||||
}
|
||||
if !isRetryableKubernetesClientError(lastErr) || time.Now().After(deadline) {
|
||||
return fmt.Errorf("ensure kubeadm:cluster-admins ClusterRoleBinding: %w", lastErr)
|
||||
}
|
||||
logger.Warn("api-server not ready while ensuring kubeadm:cluster-admins ClusterRoleBinding, retrying: %s", lastErr.Error())
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeRenewTargets(targets []string) ([]string, bool, error) {
|
||||
allowed := map[string]struct{}{
|
||||
"all": {},
|
||||
AdminConf: {},
|
||||
SuperAdminConf: {},
|
||||
ControllerConf: {},
|
||||
SchedulerConf: {},
|
||||
KubeletConf: {},
|
||||
}
|
||||
|
||||
normalized := make([]string, 0, len(targets))
|
||||
seen := make(map[string]struct{}, len(targets))
|
||||
for _, target := range targets {
|
||||
t := strings.TrimSpace(target)
|
||||
if t == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := allowed[t]; !ok {
|
||||
return nil, false, fmt.Errorf("unsupported renew target %q", t)
|
||||
}
|
||||
if _, ok := seen[t]; ok {
|
||||
continue
|
||||
}
|
||||
seen[t] = struct{}{}
|
||||
normalized = append(normalized, t)
|
||||
}
|
||||
if len(normalized) == 0 {
|
||||
return nil, false, errors.New("at least one renew target must be specified")
|
||||
}
|
||||
if _, ok := seen["all"]; ok {
|
||||
return normalized, true, nil
|
||||
}
|
||||
return normalized, false, nil
|
||||
}
|
||||
|
||||
func validateRenewGroups(targets []string, renewAll bool, groups []string) error {
|
||||
if groups == nil || renewAll {
|
||||
return nil
|
||||
}
|
||||
for _, target := range targets {
|
||||
if target == AdminConf {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("renew groups can only be used with target admin.conf or all")
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) UpdateCertSANs(certSans []string) error {
|
||||
@@ -133,7 +430,7 @@ func (k *KubeadmRuntime) InitCertsAndKubeConfigs() error {
|
||||
if err := k.CreateKubeConfigFiles(); err != nil {
|
||||
return fmt.Errorf("failed to generate kubernetes conf: %w", err)
|
||||
}
|
||||
return k.SendJoinMasterKubeConfigs(k.getMasterIPAndPortList()[:1], AdminConf, ControllerConf, SchedulerConf, KubeletConf)
|
||||
return k.SendJoinMasterKubeConfigs(k.getMasterIPAndPortList()[:1], remoteControlPlaneKubeConfigFiles(true)...)
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) initCert() error {
|
||||
@@ -167,38 +464,6 @@ func (k *KubeadmRuntime) showKubeadmCert() error {
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) deleteAPIServer() error {
|
||||
podIDSh := fmt.Sprintf("crictl ps -a --name %s -o json", kubernetes.KubeAPIServer)
|
||||
type crictlPS struct {
|
||||
Containers []struct {
|
||||
ID string `json:"id"`
|
||||
PodSandboxID string `json:"podSandboxId"`
|
||||
} `json:"containers"`
|
||||
}
|
||||
logger.Info("delete pod apiserver from crictl")
|
||||
eg, _ := errgroup.WithContext(context.Background())
|
||||
for _, master := range k.getMasterIPAndPortList() {
|
||||
m := master
|
||||
eg.Go(func() error {
|
||||
podIDJson, err := k.sshCmdToString(m, podIDSh)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ps := &crictlPS{}
|
||||
if err = json.Unmarshal([]byte(podIDJson), ps); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(ps.Containers) > 0 {
|
||||
podID := ps.Containers[0].PodSandboxID[:13]
|
||||
logger.Debug("found podID %s in %s", podID, m)
|
||||
//crictl stopp
|
||||
if err = k.sshCmdAsync(m, fmt.Sprintf("crictl --timeout=10s stopp %s", podID)); err != nil {
|
||||
return err
|
||||
}
|
||||
//crictl rmp
|
||||
return k.sshCmdAsync(m, fmt.Sprintf("crictl rmp %s", podID))
|
||||
}
|
||||
return errors.New("not found apiServer pod running")
|
||||
})
|
||||
}
|
||||
return eg.Wait()
|
||||
return k.deleteStaticPod(kubernetes.KubeAPIServer)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
package kubernetes
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestValidateRenewGroups(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
targets []string
|
||||
renewAll bool
|
||||
groups []string
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "allow nil groups",
|
||||
targets: []string{ControllerConf},
|
||||
renewAll: false,
|
||||
groups: nil,
|
||||
},
|
||||
{
|
||||
name: "allow all target",
|
||||
targets: []string{"all"},
|
||||
renewAll: true,
|
||||
groups: []string{"custom:group"},
|
||||
},
|
||||
{
|
||||
name: "allow admin target",
|
||||
targets: []string{AdminConf},
|
||||
renewAll: false,
|
||||
groups: []string{"custom:group"},
|
||||
},
|
||||
{
|
||||
name: "reject non admin targets",
|
||||
targets: []string{ControllerConf, SchedulerConf},
|
||||
renewAll: false,
|
||||
groups: []string{"custom:group"},
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateRenewGroups(tt.targets, tt.renewAll, tt.groups)
|
||||
if (err != nil) != tt.expectErr {
|
||||
t.Fatalf("validateRenewGroups() error = %v, expectErr %v", err, tt.expectErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultLocalKubeConfigFiles(t *testing.T) {
|
||||
tests := []struct {
|
||||
version string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
version: "v1.28.9",
|
||||
want: []string{AdminConf, ControllerConf, SchedulerConf, KubeletConf},
|
||||
},
|
||||
{
|
||||
version: "v1.29.0",
|
||||
want: []string{AdminConf, ControllerConf, SchedulerConf, KubeletConf, SuperAdminConf},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := defaultLocalKubeConfigFiles(tt.version)
|
||||
if len(got) != len(tt.want) {
|
||||
t.Fatalf("defaultLocalKubeConfigFiles(%q) = %v, want %v", tt.version, got, tt.want)
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tt.want[i] {
|
||||
t.Fatalf("defaultLocalKubeConfigFiles(%q) = %v, want %v", tt.version, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRenewTargetsAllowsSuperAdmin(t *testing.T) {
|
||||
targets, renewAll, err := normalizeRenewTargets([]string{SuperAdminConf})
|
||||
if err != nil {
|
||||
t.Fatalf("normalizeRenewTargets() error = %v", err)
|
||||
}
|
||||
if renewAll {
|
||||
t.Fatal("expected super-admin target not to imply renewAll")
|
||||
}
|
||||
if len(targets) != 1 || targets[0] != SuperAdminConf {
|
||||
t.Fatalf("normalizeRenewTargets() = %v, want [%s]", targets, SuperAdminConf)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeRenewTargetsRejectsSystemCertificates(t *testing.T) {
|
||||
if _, _, err := normalizeRenewTargets([]string{"apiserver-kubelet-client"}); err == nil {
|
||||
t.Fatal("expected system certificate targets to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveLocalKubeConfigRenewTargets(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
targets []string
|
||||
version string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "pre v129 keeps admin target only",
|
||||
targets: []string{AdminConf},
|
||||
version: "v1.28.9",
|
||||
want: []string{AdminConf},
|
||||
},
|
||||
{
|
||||
name: "v129 adds super admin alongside admin",
|
||||
targets: []string{AdminConf},
|
||||
version: "v1.29.0",
|
||||
want: []string{AdminConf, SuperAdminConf},
|
||||
},
|
||||
{
|
||||
name: "v129 does not duplicate explicit super admin target",
|
||||
targets: []string{AdminConf, SuperAdminConf},
|
||||
version: "v1.29.0",
|
||||
want: []string{AdminConf, SuperAdminConf},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := effectiveLocalKubeConfigRenewTargets(tt.targets, tt.version)
|
||||
if len(got) != len(tt.want) {
|
||||
t.Fatalf("effectiveLocalKubeConfigRenewTargets(%v, %q) = %v, want %v", tt.targets, tt.version, got, tt.want)
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tt.want[i] {
|
||||
t.Fatalf("effectiveLocalKubeConfigRenewTargets(%v, %q) = %v, want %v", tt.targets, tt.version, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteControlPlaneKubeConfigFilesExcludeSuperAdmin(t *testing.T) {
|
||||
tests := []struct {
|
||||
includeKubelet bool
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
includeKubelet: false,
|
||||
want: []string{AdminConf, ControllerConf, SchedulerConf},
|
||||
},
|
||||
{
|
||||
includeKubelet: true,
|
||||
want: []string{AdminConf, ControllerConf, SchedulerConf, KubeletConf},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := remoteControlPlaneKubeConfigFiles(tt.includeKubelet)
|
||||
if len(got) != len(tt.want) {
|
||||
t.Fatalf("remoteControlPlaneKubeConfigFiles(%v) = %v, want %v", tt.includeKubelet, got, tt.want)
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tt.want[i] {
|
||||
t.Fatalf("remoteControlPlaneKubeConfigFiles(%v) = %v, want %v", tt.includeKubelet, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRenewGroupsAllowsAll(t *testing.T) {
|
||||
if err := validateRenewGroups([]string{"all"}, true, []string{"custom:group"}); err != nil {
|
||||
t.Fatalf("validateRenewGroups(all) error = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright © 2026 sealos.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
|
||||
"github.com/labring/sealos/pkg/runtime/kubernetes/types"
|
||||
"github.com/labring/sealos/pkg/utils/yaml"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultCertificateValidityPeriod = "876000h"
|
||||
defaultCACertificateValidityPeriod = "876000h"
|
||||
)
|
||||
|
||||
func shouldUseKubeadmV1beta4Features(version string) (bool, error) {
|
||||
sver, err := semver.NewVersion(version)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("parse kubernetes version %q: %w", version, err)
|
||||
}
|
||||
return !sver.LessThan(V1310), nil
|
||||
}
|
||||
|
||||
func marshalConfigsForVersion(version string, configs ...interface{}) ([]byte, error) {
|
||||
enableV1beta4Features, err := shouldUseKubeadmV1beta4Features(version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
docs := make([][]byte, 0, len(configs))
|
||||
for _, cfg := range configs {
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if enableV1beta4Features {
|
||||
data, err = appendKubeadmCertValidityPeriods(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
docs = append(docs, data)
|
||||
}
|
||||
return bytes.Join(docs, []byte("\n---\n")), nil
|
||||
}
|
||||
|
||||
func appendKubeadmCertValidityPeriods(raw []byte) ([]byte, error) {
|
||||
doc, err := yaml.UnmarshalToMap(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
kind, _ := doc["kind"].(string)
|
||||
apiVersion, _ := doc["apiVersion"].(string)
|
||||
if kind != "ClusterConfiguration" || apiVersion != types.KubeadmV1beta4 {
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
var additions []string
|
||||
if _, ok := doc["certificateValidityPeriod"]; !ok {
|
||||
additions = append(additions, fmt.Sprintf("certificateValidityPeriod: %s", defaultCertificateValidityPeriod))
|
||||
}
|
||||
if _, ok := doc["caCertificateValidityPeriod"]; !ok {
|
||||
additions = append(additions, fmt.Sprintf("caCertificateValidityPeriod: %s", defaultCACertificateValidityPeriod))
|
||||
}
|
||||
if len(additions) == 0 {
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
if !bytes.HasSuffix(raw, []byte("\n")) {
|
||||
raw = append(raw, '\n')
|
||||
}
|
||||
return append(raw, []byte(strings.Join(additions, "\n")+"\n")...), nil
|
||||
}
|
||||
@@ -57,7 +57,7 @@ func (k *KubeadmRuntime) GenerateCert() error {
|
||||
hostName,
|
||||
k.getServiceCIDR(),
|
||||
k.getDNSDomain())
|
||||
return cert.GenerateCert(
|
||||
return cert.GenerateCertForKubeVersion(
|
||||
k.pathResolver.PkiPath(),
|
||||
k.pathResolver.PkiEtcdPath(),
|
||||
k.getCertSANs(),
|
||||
@@ -65,6 +65,7 @@ func (k *KubeadmRuntime) GenerateCert() error {
|
||||
hostName,
|
||||
k.getServiceCIDR(),
|
||||
k.getDNSDomain(),
|
||||
k.localKubeVersion(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -79,8 +80,8 @@ func (k *KubeadmRuntime) CreateKubeConfigFiles() error {
|
||||
BaseName: "ca",
|
||||
}
|
||||
|
||||
err = cert.CreateJoinControlPlaneKubeConfigFiles(k.pathResolver.EtcPath(),
|
||||
certConfig, hostName, k.getClusterAPIServer(), "kubernetes")
|
||||
err = cert.CreateJoinControlPlaneKubeConfigFilesForKubeVersion(k.pathResolver.EtcPath(),
|
||||
certConfig, hostName, k.getClusterAPIServer(), "kubernetes", k.localKubeVersion())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate kubeconfig: %v", err)
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ var (
|
||||
V1260 = semver.MustParse("v1.26.0")
|
||||
V1270 = semver.MustParse("v1.27.0")
|
||||
V1280 = semver.MustParse("v1.28.0")
|
||||
V1290 = semver.MustParse("v1.29.0")
|
||||
V1300 = semver.MustParse("v1.30.0")
|
||||
V1310 = semver.MustParse("v1.31.0")
|
||||
)
|
||||
|
||||
@@ -477,7 +479,8 @@ func (k *KubeadmRuntime) generateInitConfigs() ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return yaml.MarshalConfigs(&conversion.InitConfiguration,
|
||||
return marshalConfigsForVersion(k.kubeadmConfig.ClusterConfiguration.KubernetesVersion,
|
||||
&conversion.InitConfiguration,
|
||||
&conversion.ClusterConfiguration,
|
||||
&conversion.KubeletConfiguration,
|
||||
&conversion.KubeProxyConfiguration)
|
||||
|
||||
@@ -18,6 +18,9 @@ package kubernetes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
@@ -45,3 +48,47 @@ func (k *KubeadmRuntime) copyKubeConfigFileToNodes(hosts ...string) error {
|
||||
func (k *KubeadmRuntime) copyMasterKubeConfig(host string) error {
|
||||
return k.sshCmdAsync(host, copyKubeAdminConfigCommand)
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) syncLocalAdminKubeConfigCopies() error {
|
||||
hosts := append([]string{}, k.getMasterIPAndPortList()...)
|
||||
hosts = append(hosts, k.getNodeIPAndPortList()...)
|
||||
if len(hosts) == 0 {
|
||||
return nil
|
||||
}
|
||||
return k.copyKubeConfigFileToNodes(hosts...)
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) deleteStaticPod(component string) error {
|
||||
podIDSh := fmt.Sprintf("crictl ps -a --name %s -o json", component)
|
||||
type crictlPS struct {
|
||||
Containers []struct {
|
||||
ID string `json:"id"`
|
||||
PodSandboxID string `json:"podSandboxId"`
|
||||
} `json:"containers"`
|
||||
}
|
||||
|
||||
eg, _ := errgroup.WithContext(context.Background())
|
||||
for _, master := range k.getMasterIPAndPortList() {
|
||||
m := master
|
||||
eg.Go(func() error {
|
||||
podIDJSON, err := k.sshCmdToString(m, podIDSh)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ps := &crictlPS{}
|
||||
if err = json.Unmarshal([]byte(podIDJSON), ps); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(ps.Containers) == 0 {
|
||||
return errors.New("not found static pod running")
|
||||
}
|
||||
|
||||
podID := ps.Containers[0].PodSandboxID[:13]
|
||||
if err = k.sshCmdAsync(m, fmt.Sprintf("crictl --timeout=10s stopp %s", podID)); err != nil {
|
||||
return err
|
||||
}
|
||||
return k.sshCmdAsync(m, fmt.Sprintf("crictl rmp %s", podID))
|
||||
})
|
||||
}
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
clientkubernetes "github.com/labring/sealos/pkg/client-go/kubernetes"
|
||||
"github.com/labring/sealos/pkg/constants"
|
||||
"github.com/labring/sealos/pkg/ssh"
|
||||
v1beta1 "github.com/labring/sealos/pkg/types/v1beta1"
|
||||
)
|
||||
|
||||
func TestSyncLocalAdminKubeConfigCopies(t *testing.T) {
|
||||
rootDir := t.TempDir()
|
||||
prevRuntimeRoot := constants.DefaultRuntimeRootDir
|
||||
constants.DefaultRuntimeRootDir = rootDir
|
||||
t.Cleanup(func() {
|
||||
constants.DefaultRuntimeRootDir = prevRuntimeRoot
|
||||
})
|
||||
|
||||
stub := &stubSSH{
|
||||
cmdToStringResponses: map[string]string{
|
||||
"master0|echo $HOME": "/root/master0",
|
||||
"master1|echo $HOME": "/root/master1",
|
||||
"node0|echo $HOME": "/home/node0",
|
||||
},
|
||||
}
|
||||
rt := &KubeadmRuntime{
|
||||
execer: stub,
|
||||
cluster: testClusterWithNodes([]string{"master0", "master1"}, []string{"node0"}),
|
||||
pathResolver: constants.NewPathResolver("test-cluster"),
|
||||
}
|
||||
|
||||
if err := rt.syncLocalAdminKubeConfigCopies(); err != nil {
|
||||
t.Fatalf("syncLocalAdminKubeConfigCopies() error = %v", err)
|
||||
}
|
||||
|
||||
want := []string{
|
||||
"master0|" + filepath.Join(rootDir, "test-cluster", "etc", "admin.conf") + "|/root/master0/.kube/config",
|
||||
"master1|" + filepath.Join(rootDir, "test-cluster", "etc", "admin.conf") + "|/root/master1/.kube/config",
|
||||
"node0|" + filepath.Join(rootDir, "test-cluster", "etc", "admin.conf") + "|/home/node0/.kube/config",
|
||||
}
|
||||
sort.Strings(want)
|
||||
got := append([]string{}, stub.copyCalls...)
|
||||
sort.Strings(got)
|
||||
if len(stub.copyCalls) != len(want) {
|
||||
t.Fatalf("syncLocalAdminKubeConfigCopies() copyCalls = %v, want %v", stub.copyCalls, want)
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("syncLocalAdminKubeConfigCopies() copyCalls = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteStaticPodMissingContainerError(t *testing.T) {
|
||||
stub := &stubSSH{
|
||||
cmdToStringResponses: map[string]string{
|
||||
"master0|crictl ps -a --name kube-scheduler -o json": `{"containers":[]}`,
|
||||
},
|
||||
}
|
||||
rt := &KubeadmRuntime{
|
||||
execer: stub,
|
||||
cluster: testCluster([]string{"master0"}),
|
||||
}
|
||||
|
||||
err := rt.deleteStaticPod(clientkubernetes.KubeScheduler)
|
||||
if err == nil {
|
||||
t.Fatal("expected deleteStaticPod to fail when no container is returned")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "not found static pod running") {
|
||||
t.Fatalf("deleteStaticPod() error = %v, want missing static pod error", err)
|
||||
}
|
||||
}
|
||||
|
||||
type stubSSH struct {
|
||||
cmdToStringResponses map[string]string
|
||||
copyCalls []string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
var _ ssh.Interface = (*stubSSH)(nil)
|
||||
|
||||
func (s *stubSSH) Copy(host, src, dst string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.copyCalls = append(s.copyCalls, host+"|"+src+"|"+dst)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stubSSH) Fetch(host, src, dst string) error { return nil }
|
||||
|
||||
func (s *stubSSH) CmdAsync(host string, cmds ...string) error { return nil }
|
||||
|
||||
func (s *stubSSH) CmdAsyncWithContext(ctx context.Context, host string, cmds ...string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stubSSH) Cmd(host, cmd string) ([]byte, error) { return nil, nil }
|
||||
|
||||
func (s *stubSSH) CmdToString(host, cmd, spilt string) (string, error) {
|
||||
if got, ok := s.cmdToStringResponses[host+"|"+cmd]; ok {
|
||||
return got, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (s *stubSSH) Ping(host string) error { return nil }
|
||||
|
||||
func testCluster(masters []string) *v1beta1.Cluster {
|
||||
return testClusterWithNodes(masters, nil)
|
||||
}
|
||||
|
||||
func testClusterWithNodes(masters, nodes []string) *v1beta1.Cluster {
|
||||
host := v1beta1.Host{
|
||||
Roles: []string{v1beta1.MASTER},
|
||||
IPS: masters,
|
||||
}
|
||||
hosts := []v1beta1.Host{host}
|
||||
if len(nodes) != 0 {
|
||||
hosts = append(hosts, v1beta1.Host{
|
||||
Roles: []string{v1beta1.NODE},
|
||||
IPS: nodes,
|
||||
})
|
||||
}
|
||||
return &v1beta1.Cluster{
|
||||
Spec: v1beta1.ClusterSpec{
|
||||
Hosts: hosts,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,10 @@ func (k *KubeadmRuntime) imagePull(hostAndPort, version string) error {
|
||||
if version == "" {
|
||||
version = k.getKubeVersion()
|
||||
}
|
||||
registry := helpers.GetRegistryInfo(k.execer, k.pathResolver.RootFSPath(), k.cluster.GetRegistryIPAndPort())
|
||||
if err := helpers.WaitRegistryReady(k.execer, hostAndPort, registry.Domain, registry.Port); err != nil {
|
||||
return err
|
||||
}
|
||||
type Images struct {
|
||||
Images []string `json:"images"`
|
||||
}
|
||||
@@ -69,7 +73,6 @@ func (k *KubeadmRuntime) imagePull(hostAndPort, version string) error {
|
||||
return fmt.Errorf("unmarshal kubeadm images list failed, json: %s, error: %s", listJson, err.Error())
|
||||
}
|
||||
var newImageList []string
|
||||
registry := helpers.GetRegistryInfo(k.execer, k.pathResolver.RootFSPath(), k.cluster.GetRegistryIPAndPort())
|
||||
for _, image := range images.Images {
|
||||
image = strings.TrimSpace(image)
|
||||
if image == "" {
|
||||
@@ -140,14 +143,8 @@ func (k *KubeadmRuntime) joinMasters(masters []string) error {
|
||||
if err = k.copyStaticFiles(masters); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = k.SendJoinMasterKubeConfigs(masters, AdminConf, ControllerConf, SchedulerConf); err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO only needs send ca?
|
||||
if err = k.sendNewCertAndKey(masters); err != nil {
|
||||
return err
|
||||
}
|
||||
// setKubernetesToken uploads control-plane shared certs through kubeadm
|
||||
// and embeds the matching certificateKey into the join config.
|
||||
if err = k.setKubernetesToken(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -167,11 +164,6 @@ func (k *KubeadmRuntime) joinMasters(masters []string) error {
|
||||
if err = k.imagePull(master, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Debug("start to generate cert for master %s", master)
|
||||
err = k.execCert(master)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create cert for master %s: %v", master, err)
|
||||
}
|
||||
|
||||
err = k.sshCmdAsync(master, joinCmd)
|
||||
if err != nil {
|
||||
|
||||
@@ -15,11 +15,14 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
coreV1 "k8s.io/api/core/v1"
|
||||
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/labring/sealos/pkg/client-go/kubernetes"
|
||||
"github.com/labring/sealos/pkg/constants"
|
||||
@@ -28,7 +31,6 @@ import (
|
||||
"github.com/labring/sealos/pkg/ssh"
|
||||
v2 "github.com/labring/sealos/pkg/types/v1beta1"
|
||||
"github.com/labring/sealos/pkg/utils/logger"
|
||||
"github.com/labring/sealos/pkg/utils/yaml"
|
||||
)
|
||||
|
||||
type KubeadmRuntime struct {
|
||||
@@ -79,7 +81,7 @@ func (k *KubeadmRuntime) GetRawConfig() ([]byte, error) {
|
||||
conversion.KubeProxyConfiguration,
|
||||
conversion.KubeletConfiguration,
|
||||
}
|
||||
data, err := yaml.MarshalConfigs(objects...)
|
||||
data, err := marshalConfigsForVersion(k.kubeadmConfig.ClusterConfiguration.KubernetesVersion, objects...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -181,16 +183,55 @@ func (k *KubeadmRuntime) Upgrade(version string) error {
|
||||
return err
|
||||
}
|
||||
if v0.Equal(v1) {
|
||||
logger.Info("skip upgrade because of same version")
|
||||
return nil
|
||||
}
|
||||
|
||||
if v0.GreaterThan(v1) {
|
||||
return fmt.Errorf("cannot apply an older version %s than %s", version, currVersion)
|
||||
}
|
||||
if v0.Minor()+1 < v1.Minor() {
|
||||
return fmt.Errorf("cannot be upgraded across more than one major releases, %s -> %s", currVersion, version)
|
||||
needUpgrade, err := k.nodeVersionsNeedUpgrade(v1)
|
||||
if err != nil {
|
||||
logger.Warn("failed to check node kubelet versions: %s", err.Error())
|
||||
logger.Info("skip upgrade because of same version")
|
||||
return nil
|
||||
}
|
||||
if !needUpgrade {
|
||||
logger.Info("skip upgrade because of same version")
|
||||
return nil
|
||||
}
|
||||
logger.Info("continue upgrade because some node kubelet versions or readiness states are not aligned with %s", version)
|
||||
} else {
|
||||
if v0.GreaterThan(v1) {
|
||||
return fmt.Errorf("cannot apply an older version %s than %s", version, currVersion)
|
||||
}
|
||||
if v0.Minor()+1 < v1.Minor() {
|
||||
return fmt.Errorf("cannot be upgraded across more than one major releases, %s -> %s", currVersion, version)
|
||||
}
|
||||
}
|
||||
|
||||
return k.upgradeCluster(version)
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) nodeVersionsNeedUpgrade(targetVersion *semver.Version) (bool, error) {
|
||||
client, err := k.getKubeInterface()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
nodes, err := client.Kubernetes().CoreV1().Nodes().List(context.TODO(), metaV1.ListOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, node := range nodes.Items {
|
||||
kubeletVersion := node.Status.NodeInfo.KubeletVersion
|
||||
nodeVersion, err := semver.NewVersion(kubeletVersion)
|
||||
if err != nil {
|
||||
logger.Warn("failed to parse kubelet version %q of node %s: %s", kubeletVersion, node.Name, err.Error())
|
||||
return true, nil
|
||||
}
|
||||
if !nodeVersion.Equal(targetVersion) {
|
||||
logger.Info("node %s kubelet version %s does not match target %s", node.Name, kubeletVersion, targetVersion.Original())
|
||||
return true, nil
|
||||
}
|
||||
for _, condition := range node.Status.Conditions {
|
||||
if condition.Type == coreV1.NodeReady && condition.Status == coreV1.ConditionUnknown {
|
||||
logger.Info("node %s ready condition is unknown, continue upgrade recovery", node.Name)
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -148,11 +148,19 @@ func (k *KubeadmRuntime) sshCopy(host, srcFilePath, dstFilePath string) error {
|
||||
return k.execer.Copy(host, srcFilePath, dstFilePath)
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) sshFetch(host, srcFilePath, dstFilePath string) error {
|
||||
return k.execer.Fetch(host, srcFilePath, dstFilePath)
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) clearKubeClient() {
|
||||
k.cli = nil
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) getKubeInterface() (kubernetes.Client, error) {
|
||||
if k.cli != nil {
|
||||
return k.cli, nil
|
||||
}
|
||||
cli, err := kubernetes.NewKubernetesClient(k.pathResolver.AdminFile(), k.getMaster0IPAPIServer())
|
||||
cli, err := kubernetes.NewKubernetesClient(k.pathResolver.AdminFile(), k.getClusterAPIServer())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ package kubernetes
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -26,6 +28,7 @@ import (
|
||||
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
|
||||
|
||||
"github.com/labring/sealos/pkg/cert"
|
||||
"github.com/labring/sealos/pkg/runtime/decode"
|
||||
"github.com/labring/sealos/pkg/runtime/kubernetes/types"
|
||||
"github.com/labring/sealos/pkg/utils/logger"
|
||||
@@ -33,8 +36,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
upgradeApplyCmd = "kubeadm upgrade apply --certificate-renewal=false --config %s --yes"
|
||||
upradeNodeCmd = "kubeadm upgrade node --certificate-renewal=false --skip-phases preflight"
|
||||
upgradeApplyCmd = "kubeadm upgrade apply %s --yes --ignore-preflight-errors=SystemVerification,ControlPlaneNodesReady,CreateJob"
|
||||
upgradeApplyCmdNoCertRenewal = "kubeadm upgrade apply %s --certificate-renewal=false --yes --ignore-preflight-errors=SystemVerification,ControlPlaneNodesReady,CreateJob"
|
||||
upgradeNodeCmd = "kubeadm upgrade node --skip-phases preflight"
|
||||
upgradeNodeCmdNoCertRenewal = "kubeadm upgrade node --certificate-renewal=false --skip-phases preflight"
|
||||
//drainNodeCmd = "kubectl drain %s --ignore-daemonsets"
|
||||
cordonNodeCmd = "kubectl cordon %s"
|
||||
uncordonNodeCmd = "kubectl uncordon %s"
|
||||
@@ -44,18 +49,34 @@ const (
|
||||
installKubeadmCmd = "cp -rf %s/kubeadm /usr/bin"
|
||||
installKubeletCmd = "cp -rf %s/kubelet /usr/bin"
|
||||
installKubectlCmd = "cp -rf %s/kubectl /usr/bin"
|
||||
renewCertCmd = "kubeadm certs renew %s"
|
||||
|
||||
writeKubeadmConfig = `cat > %s << EOF
|
||||
%s
|
||||
EOF`
|
||||
|
||||
kubeletConfigPath = "/var/lib/kubelet/config.yaml"
|
||||
apiserverKubeletClientCertName = "apiserver-kubelet-client"
|
||||
apiserverEtcdClientCertName = "apiserver-etcd-client"
|
||||
etcdHealthcheckClientCertName = "etcd-healthcheck-client"
|
||||
adminKubeConfigName = "admin.conf"
|
||||
)
|
||||
|
||||
type remoteCertMigration struct {
|
||||
name string
|
||||
phaseName string
|
||||
files []string
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) upgradeCluster(version string) error {
|
||||
logger.Info("Change ClusterConfiguration up to newVersion if need.")
|
||||
conversion, err := k.autoUpdateConfig(version)
|
||||
conversion, hasLocalEtcd, err := k.autoUpdateConfig(version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := k.runUpgradeMigrations(conversion, version, hasLocalEtcd); err != nil {
|
||||
return err
|
||||
}
|
||||
//upgrade master0
|
||||
logger.Info("start to upgrade master0")
|
||||
err = k.upgradeMaster0(conversion, version)
|
||||
@@ -71,12 +92,455 @@ func (k *KubeadmRuntime) upgradeCluster(version string) error {
|
||||
upgradeNodes = append(upgradeNodes, node)
|
||||
}
|
||||
logger.Info("start to upgrade other control-planes and worker nodes")
|
||||
return k.upgradeOtherNodes(upgradeNodes, version)
|
||||
if err := k.upgradeOtherNodes(conversion, upgradeNodes, version); err != nil {
|
||||
return err
|
||||
}
|
||||
return k.syncLocalCertificateIdentity(version)
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) runUpgradeMigrations(conversion *types.ConvertedKubeadmConfig, version string, hasLocalEtcd bool) error {
|
||||
currentVersion := k.getKubeVersionFromImage()
|
||||
migrations, err := getRemoteCertMigrations(currentVersion, version, hasLocalEtcd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(migrations) == 0 {
|
||||
return nil
|
||||
}
|
||||
configPath, err := k.stageUpgradeMigrationConfig(version, conversion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, migration := range migrations {
|
||||
if err := k.migrateRemoteCert(migration, configPath, version); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRemoteCertMigrations(currentVersion, targetVersion string, hasLocalEtcd bool) ([]remoteCertMigration, error) {
|
||||
current, err := semver.NewVersion(currentVersion)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse current kubernetes version %q: %w", currentVersion, err)
|
||||
}
|
||||
target, err := semver.NewVersion(targetVersion)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse target kubernetes version %q: %w", targetVersion, err)
|
||||
}
|
||||
if !current.LessThan(V1290) || target.LessThan(V1290) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
migrations := []remoteCertMigration{
|
||||
{
|
||||
name: apiserverKubeletClientCertName,
|
||||
phaseName: apiserverKubeletClientCertName,
|
||||
files: []string{
|
||||
apiserverKubeletClientCertName + ".crt",
|
||||
apiserverKubeletClientCertName + ".key",
|
||||
},
|
||||
},
|
||||
}
|
||||
if hasLocalEtcd {
|
||||
migrations = append(migrations,
|
||||
remoteCertMigration{
|
||||
name: apiserverEtcdClientCertName,
|
||||
phaseName: apiserverEtcdClientCertName,
|
||||
files: []string{
|
||||
apiserverEtcdClientCertName + ".crt",
|
||||
apiserverEtcdClientCertName + ".key",
|
||||
},
|
||||
},
|
||||
remoteCertMigration{
|
||||
name: etcdHealthcheckClientCertName,
|
||||
phaseName: etcdHealthcheckClientCertName,
|
||||
files: []string{
|
||||
"etcd/healthcheck-client.crt",
|
||||
"etcd/healthcheck-client.key",
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
return migrations, nil
|
||||
}
|
||||
|
||||
func shouldMigrateAPIServerKubeletClientCert(currentVersion, targetVersion string) (bool, error) {
|
||||
migrations, err := getRemoteCertMigrations(currentVersion, targetVersion, false)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, migration := range migrations {
|
||||
if migration.name == apiserverKubeletClientCertName {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) migrateRemoteCert(migration remoteCertMigration, configPath, version string) error {
|
||||
logger.Info("regenerate %s on master0 before upgrading to %s", migration.name, version)
|
||||
var stageFile func(string) (string, error)
|
||||
if shouldUseKubeadmV1beta4Features, err := shouldUseKubeadmV1beta4Features(version); err != nil {
|
||||
return err
|
||||
} else if shouldUseKubeadmV1beta4Features {
|
||||
master0 := k.getMaster0IPAndPort()
|
||||
kubeBinaryPath := k.pathResolver.RootFSBinPath()
|
||||
if err := k.sshCmdAsyncSeq(
|
||||
master0,
|
||||
fmt.Sprintf(installKubeadmCmd, kubeBinaryPath),
|
||||
buildRegenerateRemoteCertCmd(migration.phaseName, configPath, migration.files),
|
||||
); err != nil {
|
||||
return fmt.Errorf("regenerate %s on master0: %w", migration.name, err)
|
||||
}
|
||||
stageFile = k.stagePKIFileFromMaster0
|
||||
} else {
|
||||
stagingDir, err := k.regenerateRemoteCertWithLocalIdentityModel(migration, version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stageFile = func(fileName string) (string, error) {
|
||||
return stagedLocalPKIFile(stagingDir, fileName)
|
||||
}
|
||||
}
|
||||
|
||||
for _, fileName := range migration.files {
|
||||
stagedFile, err := stageFile(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := k.sendFileToHosts(k.getMasterIPAndPortList(), stagedFile, path.Join(kubernetesEtcPKI, fileName)); err != nil {
|
||||
return fmt.Errorf("sync %s to control-plane nodes: %w", migration.name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stagedLocalPKIFile(stagingDir, fileName string) (string, error) {
|
||||
stagedFile := filepath.Join(stagingDir, filepath.FromSlash(fileName))
|
||||
if _, err := os.Stat(stagedFile); err != nil {
|
||||
return "", fmt.Errorf("local regenerated PKI file %s: %w", stagedFile, err)
|
||||
}
|
||||
return stagedFile, nil
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) regenerateRemoteCertWithLocalIdentityModel(migration remoteCertMigration, version string) (string, error) {
|
||||
if err := k.mergeWithBuiltinKubeadmConfig(); err != nil {
|
||||
return "", fmt.Errorf("load cluster networking and certSANs for %s migration: %w", migration.name, err)
|
||||
}
|
||||
|
||||
stagingDir := filepath.Join(k.pathResolver.TmpPath(), "upgrade-migrations", "remote-cert-regeneration")
|
||||
if err := os.RemoveAll(stagingDir); err != nil {
|
||||
return "", fmt.Errorf("cleanup local cert staging dir %s: %w", stagingDir, err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(stagingDir, "etcd"), 0o755); err != nil {
|
||||
return "", fmt.Errorf("create local cert staging dir %s: %w", stagingDir, err)
|
||||
}
|
||||
|
||||
for _, caFile := range []string{
|
||||
"ca.crt", "ca.key",
|
||||
"front-proxy-ca.crt", "front-proxy-ca.key",
|
||||
"etcd/ca.crt", "etcd/ca.key",
|
||||
"sa.key", "sa.pub",
|
||||
} {
|
||||
src := path.Join(kubernetesEtcPKI, caFile)
|
||||
dst := filepath.Join(stagingDir, filepath.FromSlash(caFile))
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||
return "", fmt.Errorf("create local dir for %s: %w", caFile, err)
|
||||
}
|
||||
if err := k.sshFetch(k.getMaster0IPAndPort(), src, dst); err != nil {
|
||||
return "", fmt.Errorf("fetch %s from master0: %w", caFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
hostName, err := k.execHostname(k.getMaster0IPAndPort())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get master0 hostname for %s migration: %w", migration.name, err)
|
||||
}
|
||||
|
||||
if err := cert.RenewLeafCertsForKubeVersion(
|
||||
stagingDir,
|
||||
filepath.Join(stagingDir, "etcd"),
|
||||
k.getCertSANs(),
|
||||
k.getMaster0IP(),
|
||||
hostName,
|
||||
k.getServiceCIDR(),
|
||||
k.getDNSDomain(),
|
||||
version,
|
||||
); err != nil {
|
||||
return "", fmt.Errorf("regenerate %s with local identity model: %w", migration.name, err)
|
||||
}
|
||||
return stagingDir, nil
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) stageUpgradeMigrationConfig(version string, conversion *types.ConvertedKubeadmConfig) (string, error) {
|
||||
data, err := marshalConfigsForVersion(version, &conversion.InitConfiguration, &conversion.ClusterConfiguration)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshal kubeadm migration config for %s: %w", version, err)
|
||||
}
|
||||
|
||||
stagingDir := path.Join(k.pathResolver.TmpPath(), "upgrade-migrations")
|
||||
if err := os.MkdirAll(stagingDir, 0o755); err != nil {
|
||||
return "", fmt.Errorf("create migration staging dir %s: %w", stagingDir, err)
|
||||
}
|
||||
|
||||
localConfigPath := k.upgradeMigrationConfigLocalPath()
|
||||
if err := os.WriteFile(localConfigPath, data, 0o600); err != nil {
|
||||
return "", fmt.Errorf("write local migration config %s: %w", localConfigPath, err)
|
||||
}
|
||||
|
||||
master0 := k.getMaster0IPAndPort()
|
||||
remoteConfigPath := k.remoteUpgradeMigrationConfigPath()
|
||||
if err := k.sshCmdAsync(master0, fmt.Sprintf("mkdir -p %s", remoteConfigDir)); err != nil {
|
||||
return "", fmt.Errorf("create remote migration dir %s on master0: %w", remoteConfigDir, err)
|
||||
}
|
||||
if err := k.sshCopy(master0, localConfigPath, remoteConfigPath); err != nil {
|
||||
return "", fmt.Errorf("copy migration config to master0: %w", err)
|
||||
}
|
||||
|
||||
return remoteConfigPath, nil
|
||||
}
|
||||
|
||||
const remoteConfigDir = "/tmp/sealos-upgrade-migrations"
|
||||
|
||||
func (k *KubeadmRuntime) upgradeMigrationConfigLocalPath() string {
|
||||
return path.Join(k.pathResolver.TmpPath(), "upgrade-migrations", "kubeadm-migration.yaml")
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) remoteUpgradeMigrationConfigPath() string {
|
||||
return path.Join(remoteConfigDir, "kubeadm-migration.yaml")
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) syncUpgradeMigrationConfig(hosts []string) error {
|
||||
localConfigPath := k.upgradeMigrationConfigLocalPath()
|
||||
remoteConfigPath := k.remoteUpgradeMigrationConfigPath()
|
||||
for _, host := range hosts {
|
||||
if err := k.sshCmdAsync(host, fmt.Sprintf("mkdir -p %s", remoteConfigDir)); err != nil {
|
||||
return fmt.Errorf("create remote migration dir %s on %s: %w", remoteConfigDir, host, err)
|
||||
}
|
||||
if err := k.sshCopy(host, localConfigPath, remoteConfigPath); err != nil {
|
||||
return fmt.Errorf("copy migration config to %s: %w", host, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) stagePKIFileFromMaster0(fileName string) (string, error) {
|
||||
stagingDir := path.Join(k.pathResolver.TmpPath(), "upgrade-migrations")
|
||||
if err := os.MkdirAll(stagingDir, 0o755); err != nil {
|
||||
return "", fmt.Errorf("create migration staging dir %s: %w", stagingDir, err)
|
||||
}
|
||||
|
||||
stagedFile := path.Join(stagingDir, fileName)
|
||||
if err := os.MkdirAll(path.Dir(stagedFile), 0o755); err != nil {
|
||||
return "", fmt.Errorf("create local staging dir for %s: %w", fileName, err)
|
||||
}
|
||||
if err := k.sshFetch(k.getMaster0IPAndPort(), path.Join(kubernetesEtcPKI, fileName), stagedFile); err != nil {
|
||||
return "", fmt.Errorf("fetch %s from master0: %w", fileName, err)
|
||||
}
|
||||
return stagedFile, nil
|
||||
}
|
||||
|
||||
func buildRegenerateRemoteCertCmd(phaseName, configPath string, files []string) string {
|
||||
var script strings.Builder
|
||||
script.WriteString("set -e; ")
|
||||
script.WriteString("backup_dir=$(mktemp -d /tmp/sealos-cert-migration-XXXXXX); ")
|
||||
for _, file := range files {
|
||||
remoteFile := path.Join(kubernetesEtcPKI, file)
|
||||
backupDir := path.Dir(file)
|
||||
if backupDir != "." {
|
||||
script.WriteString(fmt.Sprintf("mkdir -p \"$backup_dir/%s\"; ", backupDir))
|
||||
}
|
||||
script.WriteString(fmt.Sprintf("if [ -f %s ]; then cp -f %s \"$backup_dir/%s\"; fi; ", remoteFile, remoteFile, file))
|
||||
script.WriteString(fmt.Sprintf("rm -f %s; ", remoteFile))
|
||||
}
|
||||
script.WriteString(fmt.Sprintf("if ! kubeadm init phase certs %s --config %s; then ", phaseName, configPath))
|
||||
for _, file := range files {
|
||||
remoteFile := path.Join(kubernetesEtcPKI, file)
|
||||
script.WriteString(fmt.Sprintf("if [ -f \"$backup_dir/%s\" ]; then mkdir -p %s; mv -f \"$backup_dir/%s\" %s; fi; ", file, path.Dir(remoteFile), file, remoteFile))
|
||||
}
|
||||
script.WriteString("exit 1; fi; ")
|
||||
script.WriteString("rm -rf \"$backup_dir\"")
|
||||
return fmt.Sprintf("bash -c '%s'", script.String())
|
||||
}
|
||||
|
||||
func buildRegenerateRemoteAdminKubeConfigCmd(configPath string) string {
|
||||
var script strings.Builder
|
||||
script.WriteString("set -e; ")
|
||||
script.WriteString("backup_dir=$(mktemp -d /tmp/sealos-kubeconfig-migration-XXXXXX); ")
|
||||
script.WriteString(fmt.Sprintf("if [ -f /etc/kubernetes/%s ]; then cp -f /etc/kubernetes/%s \"$backup_dir/%s\"; fi; ", adminKubeConfigName, adminKubeConfigName, adminKubeConfigName))
|
||||
script.WriteString(fmt.Sprintf("rm -f /etc/kubernetes/%s; ", adminKubeConfigName))
|
||||
script.WriteString(fmt.Sprintf("if ! kubeadm init phase kubeconfig admin --config %s; then ", configPath))
|
||||
script.WriteString(fmt.Sprintf("if [ -f \"$backup_dir/%s\" ]; then mv -f \"$backup_dir/%s\" /etc/kubernetes/%s; fi; ", adminKubeConfigName, adminKubeConfigName, adminKubeConfigName))
|
||||
script.WriteString("exit 1; fi; ")
|
||||
script.WriteString("rm -rf \"$backup_dir\"")
|
||||
return fmt.Sprintf("bash -c '%s'", script.String())
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) syncLocalCertificateIdentity(version string) error {
|
||||
if err := k.mergeWithBuiltinKubeadmConfig(); err != nil {
|
||||
return fmt.Errorf("load cluster networking and certSANs for local identity sync: %w", err)
|
||||
}
|
||||
|
||||
localKubeConfigFiles := defaultLocalKubeConfigFiles(version)
|
||||
// Ensure the kubeadm:cluster-admins binding before local kubeconfig renewal.
|
||||
// After renewal, local super-admin.conf may be signed by sealos-managed PKI and
|
||||
// apiserver may still be settling after control-plane static pod upgrades.
|
||||
if shouldEnsureAdminClusterRoleBinding(version, nil, true, localKubeConfigFiles) {
|
||||
if err := k.ensureAdminClusterRoleBinding(); err != nil {
|
||||
return err
|
||||
}
|
||||
k.clearKubeClient()
|
||||
}
|
||||
|
||||
hostName, err := k.execHostname(k.getMaster0IPAndPort())
|
||||
if err != nil {
|
||||
return fmt.Errorf("get hostname failed while syncing local certificate identity: %w", err)
|
||||
}
|
||||
|
||||
if err := cert.RenewLeafCertsForKubeVersion(
|
||||
k.pathResolver.PkiPath(),
|
||||
k.pathResolver.PkiEtcdPath(),
|
||||
k.getCertSANs(),
|
||||
k.getMaster0IP(),
|
||||
hostName,
|
||||
k.getServiceCIDR(),
|
||||
k.getDNSDomain(),
|
||||
version,
|
||||
); err != nil {
|
||||
return fmt.Errorf("refresh local pki identity model for %s: %w", version, err)
|
||||
}
|
||||
|
||||
if err := renewLocalKubeConfigFilesForVersion(k, hostName, version, localKubeConfigFiles, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
k.clearKubeClient()
|
||||
if shouldRegenerateRemoteAdminKubeConfig(version) {
|
||||
if err := k.syncRemoteAdminKubeConfigIdentity(version); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
k.cli = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func shouldRegenerateRemoteAdminKubeConfig(version string) bool {
|
||||
return certUsesClusterAdminsIdentityModel(version)
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) syncRemoteAdminKubeConfigIdentity(version string) error {
|
||||
useV1beta4Features, err := shouldUseKubeadmV1beta4Features(version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if useV1beta4Features {
|
||||
if k.kubeadmConfig == nil {
|
||||
return fmt.Errorf("kubeadm config is not initialized for remote admin.conf sync")
|
||||
}
|
||||
kubeadmConfig := *k.kubeadmConfig
|
||||
kubeadmConfig.SetAPIVersion(getterKubeadmAPIVersion(version))
|
||||
conversion, err := kubeadmConfig.ToConvertedKubeadmConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert kubeadm config for remote admin.conf sync: %w", err)
|
||||
}
|
||||
configPath, err := k.stageUpgradeMigrationConfig(version, conversion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := k.syncUpgradeMigrationConfig(k.getMasterIPAndPortList()[1:]); err != nil {
|
||||
return err
|
||||
}
|
||||
kubeBinaryPath := k.pathResolver.RootFSBinPath()
|
||||
for _, master := range k.getMasterIPAndPortList() {
|
||||
if err := k.sshCmdAsyncSeq(
|
||||
master,
|
||||
fmt.Sprintf(installKubeadmCmd, kubeBinaryPath),
|
||||
buildRegenerateRemoteAdminKubeConfigCmd(configPath),
|
||||
); err != nil {
|
||||
return fmt.Errorf("regenerate admin.conf on %s: %w", master, err)
|
||||
}
|
||||
if err := k.copyMasterKubeConfig(master); err != nil {
|
||||
return fmt.Errorf("refresh $HOME/.kube/config on %s: %w", master, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return k.syncRemoteAdminKubeConfigWithLocalIdentityModel(version)
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) syncRemoteAdminKubeConfigWithLocalIdentityModel(version string) error {
|
||||
// certSANs and networking were loaded earlier in syncLocalCertificateIdentity.
|
||||
stagingDir := filepath.Join(k.pathResolver.TmpPath(), "upgrade-migrations", "remote-admin-kubeconfig")
|
||||
if err := os.RemoveAll(stagingDir); err != nil {
|
||||
return fmt.Errorf("cleanup local kubeconfig staging dir %s: %w", stagingDir, err)
|
||||
}
|
||||
if err := os.MkdirAll(stagingDir, 0o755); err != nil {
|
||||
return fmt.Errorf("create local kubeconfig staging dir %s: %w", stagingDir, err)
|
||||
}
|
||||
|
||||
for _, caFile := range []string{"ca.crt", "ca.key"} {
|
||||
src := path.Join(kubernetesEtcPKI, caFile)
|
||||
dst := filepath.Join(stagingDir, caFile)
|
||||
if err := k.sshFetch(k.getMaster0IPAndPort(), src, dst); err != nil {
|
||||
return fmt.Errorf("fetch %s from master0: %w", caFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, master := range k.getMasterIPAndPortList() {
|
||||
hostName, err := k.execHostname(master)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get hostname for %s admin.conf migration: %w", master, err)
|
||||
}
|
||||
if err := cert.RenewAdminKubeConfigFileForKubeVersion(
|
||||
stagingDir,
|
||||
cert.Config{Path: stagingDir, BaseName: "ca"},
|
||||
k.getClusterAPIServer(),
|
||||
"kubernetes",
|
||||
nil,
|
||||
version,
|
||||
); err != nil {
|
||||
return fmt.Errorf("regenerate local admin.conf staging for %s (%s): %w", master, hostName, err)
|
||||
}
|
||||
if err := k.sshCopy(master, filepath.Join(stagingDir, adminKubeConfigName), path.Join("/etc/kubernetes", adminKubeConfigName)); err != nil {
|
||||
return fmt.Errorf("copy admin.conf to %s: %w", master, err)
|
||||
}
|
||||
if err := k.copyMasterKubeConfig(master); err != nil {
|
||||
return fmt.Errorf("refresh $HOME/.kube/config on %s: %w", master, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getUpgradeApplyCmd(version string) (string, error) {
|
||||
useV1beta4Features, err := shouldUseKubeadmV1beta4Features(version)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if useV1beta4Features {
|
||||
return fmt.Sprintf(upgradeApplyCmd, version), nil
|
||||
}
|
||||
return fmt.Sprintf(upgradeApplyCmdNoCertRenewal, version), nil
|
||||
}
|
||||
|
||||
func getUpgradeNodeCmd(version string) (string, error) {
|
||||
useV1beta4Features, err := shouldUseKubeadmV1beta4Features(version)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if useV1beta4Features {
|
||||
return upgradeNodeCmd, nil
|
||||
}
|
||||
return upgradeNodeCmdNoCertRenewal, nil
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) upgradeMaster0(conversion *types.ConvertedKubeadmConfig, version string) error {
|
||||
master0ip := k.getMaster0IP()
|
||||
sver := semver.MustParse(version)
|
||||
if err := k.syncKubeletConfig(master0ip, conversion, version); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gte(sver, V1260) {
|
||||
if err := k.changeCRIVersion(master0ip); err != nil {
|
||||
return err
|
||||
@@ -101,28 +565,31 @@ func (k *KubeadmRuntime) upgradeMaster0(conversion *types.ConvertedKubeadmConfig
|
||||
return err
|
||||
}
|
||||
|
||||
// install kubeadm:{version} at master0 before listing/pulling upgrade images.
|
||||
// kubeadm owns the component image matrix, so using the target kubeadm avoids
|
||||
// pre-pulling images for the previous Kubernetes minor version.
|
||||
if err = k.sshCmdAsyncSeq(master0ip, fmt.Sprintf(installKubeadmCmd, kubeBinaryPath)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// force cri to pull the image
|
||||
err = k.imagePull(master0ip, version)
|
||||
if err != nil {
|
||||
logger.Warn("image pull pre-upgrade failed: %s", err.Error())
|
||||
}
|
||||
|
||||
config, err := yaml.MarshalConfigs(&conversion.InitConfiguration, &conversion.ClusterConfiguration)
|
||||
upgradeApplyCmd, err := getUpgradeApplyCmd(version)
|
||||
if err != nil {
|
||||
logger.Error("kubeadm config marshal failed: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
upgradeConfigName := "kubeadm-upgrade.yaml"
|
||||
upgradeConfigPath := path.Join(k.pathResolver.EtcPath(), upgradeConfigName)
|
||||
|
||||
err = k.sshCmdAsync(master0ip,
|
||||
//install kubeadm:{version} at master0
|
||||
fmt.Sprintf(installKubeadmCmd, kubeBinaryPath),
|
||||
// write kubeadm config to file
|
||||
fmt.Sprintf(writeKubeadmConfig, upgradeConfigPath, string(config)),
|
||||
//execute kubeadm upgrade apply {version} at master0
|
||||
fmt.Sprintf(upgradeApplyCmd, upgradeConfigPath),
|
||||
err = k.sshCmdAsyncSeq(master0ip,
|
||||
// execute kubeadm upgrade apply {version} at master0.
|
||||
// The desired kubeadm configuration has already been persisted to the
|
||||
// cluster ConfigMap by autoUpdateConfig. Passing InitConfiguration and
|
||||
// ClusterConfiguration through --config is rejected by newer kubeadm,
|
||||
// which expects an UpgradeConfiguration for that flag.
|
||||
upgradeApplyCmd,
|
||||
//kubectl cordon <node-to-cordon>
|
||||
fmt.Sprintf(cordonNodeCmd, master0Name),
|
||||
//install kubelet:{version},kubectl{version} at master0
|
||||
@@ -138,9 +605,17 @@ func (k *KubeadmRuntime) upgradeMaster0(conversion *types.ConvertedKubeadmConfig
|
||||
return k.tryUncordonNode(master0ip, master0Name)
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) upgradeOtherNodes(ips []string, version string) error {
|
||||
func (k *KubeadmRuntime) upgradeOtherNodes(conversion *types.ConvertedKubeadmConfig, ips []string, version string) error {
|
||||
sver := semver.MustParse(version)
|
||||
upgradeNodeCmd, err := getUpgradeNodeCmd(version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ip := range ips {
|
||||
if err := k.syncKubeletConfig(ip, conversion, version); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gte(sver, V1260) {
|
||||
if err := k.changeCRIVersion(ip); err != nil {
|
||||
return err
|
||||
@@ -165,6 +640,12 @@ func (k *KubeadmRuntime) upgradeOtherNodes(ips []string, version string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// install kubeadm:{version} at the node before listing/pulling upgrade images.
|
||||
err = k.sshCmdAsyncSeq(ip, fmt.Sprintf(installKubeadmCmd, kubeBinaryPath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// force cri to pull the image
|
||||
err = k.imagePull(ip, version)
|
||||
if err != nil {
|
||||
@@ -172,17 +653,18 @@ func (k *KubeadmRuntime) upgradeOtherNodes(ips []string, version string) error {
|
||||
}
|
||||
|
||||
logger.Info("upgrade node %s", nodename)
|
||||
err = k.sshCmdAsync(ip,
|
||||
//install kubeadm:{version} at the node
|
||||
fmt.Sprintf(installKubeadmCmd, kubeBinaryPath),
|
||||
//upgrade other control-plane and nodes
|
||||
upradeNodeCmd,
|
||||
//kubectl cordon <node-to-cordon>
|
||||
// upgrade other control-plane and nodes
|
||||
err = k.tryUpgradeNode(ip, upgradeNodeCmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = k.sshCmdAsyncSeq(ip,
|
||||
// kubectl cordon <node-to-cordon>
|
||||
fmt.Sprintf(cordonNodeCmd, nodename),
|
||||
//install kubelet:{version},kubectl{version} at the node
|
||||
// install kubelet:{version},kubectl{version} at the node
|
||||
fmt.Sprintf(installKubectlCmd, kubeBinaryPath),
|
||||
fmt.Sprintf(installKubeletCmd, kubeBinaryPath),
|
||||
//reload kubelet daemon
|
||||
// reload kubelet daemon
|
||||
daemonReload,
|
||||
restartKubelet,
|
||||
)
|
||||
@@ -196,19 +678,29 @@ func (k *KubeadmRuntime) upgradeOtherNodes(ips []string, version string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) autoUpdateConfig(version string) (*types.ConvertedKubeadmConfig, error) {
|
||||
func (k *KubeadmRuntime) syncKubeletConfig(ip string, conversion *types.ConvertedKubeadmConfig, version string) error {
|
||||
kubeletConfig, err := marshalKubeletConfigForVersion(conversion.KubeletConfiguration, version)
|
||||
if err != nil {
|
||||
logger.Error("failed to encode KubeletConfiguration: %s", err)
|
||||
return err
|
||||
}
|
||||
logger.Info("sync kubelet config to node %s", ip)
|
||||
return k.sshCmdAsync(ip, fmt.Sprintf(writeKubeadmConfig, kubeletConfigPath, string(kubeletConfig)))
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) autoUpdateConfig(version string) (*types.ConvertedKubeadmConfig, bool, error) {
|
||||
exp, err := k.getKubeExpansion()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
ctx := context.Background()
|
||||
clusterCfg, err := exp.FetchKubeadmConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
kubeletCfg, err := exp.FetchKubeletConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
logger.Debug("get cluster configmap data:\n%s", clusterCfg)
|
||||
logger.Debug("get kubelet configmap data:\n%s", kubeletCfg)
|
||||
@@ -216,8 +708,9 @@ func (k *KubeadmRuntime) autoUpdateConfig(version string) (*types.ConvertedKubea
|
||||
defaultKubeadmConfig, err := types.LoadKubeadmConfigs(allConfig, false, decode.CRDFromString)
|
||||
if err != nil {
|
||||
logger.Error("failed to decode cluster kubeadm config: %s", err)
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
hasLocalEtcd := usesLocalEtcd(defaultKubeadmConfig.ClusterConfiguration.Etcd)
|
||||
defaultKubeadmConfig.InitConfiguration = kubeadm.InitConfiguration{
|
||||
TypeMeta: metaV1.TypeMeta{
|
||||
APIVersion: defaultKubeadmConfig.ClusterConfiguration.APIVersion,
|
||||
@@ -228,38 +721,94 @@ func (k *KubeadmRuntime) autoUpdateConfig(version string) (*types.ConvertedKubea
|
||||
kubeadmConfig: defaultKubeadmConfig,
|
||||
}
|
||||
kk.setKubeVersion(version)
|
||||
kk.setAPIVersion(getterKubeadmAPIVersion(version))
|
||||
kk.setFeatureGatesConfiguration()
|
||||
kk.setInitConfigurationPullPolicy(v1.PullNever)
|
||||
|
||||
conversion, err := kk.kubeadmConfig.ToConvertedKubeadmConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
newClusterData, err := yaml.MarshalConfigs(&conversion.ClusterConfiguration)
|
||||
newClusterData, err := marshalConfigsForVersion(version, &conversion.ClusterConfiguration)
|
||||
if err != nil {
|
||||
logger.Error("failed to encode ClusterConfiguration: %s", err)
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
logger.Debug("update cluster config:\n%s", string(newClusterData))
|
||||
err = exp.UpdateKubeadmConfig(ctx, string(newClusterData))
|
||||
if err != nil {
|
||||
logger.Error("failed to update kubeadm-config with k8s-client: %s", err)
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
newKubeletData, err := yaml.MarshalConfigs(&conversion.KubeletConfiguration)
|
||||
newKubeletData, err := marshalKubeletConfigForVersion(conversion.KubeletConfiguration, version)
|
||||
if err != nil {
|
||||
logger.Error("failed to encode KubeletConfiguration: %s", err)
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
logger.Debug("update kubelet config:\n%s", string(newKubeletData))
|
||||
err = exp.UpdateKubeletConfig(ctx, string(newKubeletData))
|
||||
if err != nil {
|
||||
logger.Error("failed to update kubelet-config with k8s-client: %s", err)
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return conversion, nil
|
||||
return conversion, hasLocalEtcd, nil
|
||||
}
|
||||
|
||||
func usesLocalEtcd(etcd kubeadm.Etcd) bool {
|
||||
return etcd.External == nil || len(etcd.External.Endpoints) == 0
|
||||
}
|
||||
|
||||
func marshalKubeletConfigForVersion(config interface{}, version string) ([]byte, error) {
|
||||
kubeletConfig, err := yaml.MarshalConfigs(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sanitizeKubeletConfigForVersion(kubeletConfig, version)
|
||||
}
|
||||
|
||||
func sanitizeKubeletConfigForVersion(kubeletConfig []byte, version string) ([]byte, error) {
|
||||
sver, err := semver.NewVersion(version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if gte(sver, V1300) {
|
||||
return kubeletConfig, nil
|
||||
}
|
||||
|
||||
config, err := yaml.UnmarshalToMap(kubeletConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
delete(config, "containerRuntimeEndpoint")
|
||||
delete(config, "imageMaximumGCAge")
|
||||
deleteNestedMapKey(config, "logging", "options", "text")
|
||||
return yaml.Marshal(config)
|
||||
}
|
||||
|
||||
func deleteNestedMapKey(data map[string]interface{}, keys ...string) {
|
||||
if len(keys) == 0 {
|
||||
return
|
||||
}
|
||||
current := data
|
||||
for _, key := range keys[:len(keys)-1] {
|
||||
next, ok := current[key].(map[string]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
current = next
|
||||
}
|
||||
delete(current, keys[len(keys)-1])
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) sshCmdAsyncSeq(host string, cmds ...string) error {
|
||||
for _, cmd := range cmds {
|
||||
if err := k.sshCmdAsync(host, cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) pingAPIServer() error {
|
||||
@@ -297,8 +846,27 @@ func (k *KubeadmRuntime) tryUncordonNode(ip, nodename string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) tryUpgradeNode(ip, upgradeCmd string) error {
|
||||
err := k.sshCmdAsync(ip, upgradeCmd)
|
||||
timeout := time.Now().Add(1 * time.Minute)
|
||||
for err != nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
if pingErr := k.pingAPIServer(); pingErr != nil {
|
||||
logger.Warn("api-server is not ready before retrying node upgrade: %s", pingErr.Error())
|
||||
}
|
||||
err = k.sshCmdAsync(ip, upgradeCmd)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if time.Now().After(timeout) {
|
||||
return fmt.Errorf("try upgrade node %s timeout one minute: %w", ip, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) changeCRIVersion(ip string) error {
|
||||
return k.sshCmdAsync(ip,
|
||||
return k.sshCmdAsyncSeq(ip,
|
||||
"sed -i \"s/v1alpha2/v1/\" /etc/image-cri-shim.yaml",
|
||||
"systemctl restart image-cri-shim",
|
||||
"systemctl restart kubelet",
|
||||
@@ -306,8 +874,8 @@ func (k *KubeadmRuntime) changeCRIVersion(ip string) error {
|
||||
}
|
||||
|
||||
func (k *KubeadmRuntime) changeKubeletExtraArgs(ip string) error {
|
||||
return k.sshCmdAsync(ip,
|
||||
`FILE="/etc/systemd/system/kubelet.service.d/10-kubeadm.conf" && [ -f "$FILE" ] && sed -i 's/\(--container-runtime=\|--pod-infra-container-image=\)\([^ ]*\)\?//g' "$FILE"`,
|
||||
return k.sshCmdAsyncSeq(ip,
|
||||
`for FILE in /etc/systemd/system/kubelet.service.d/10-kubeadm.conf /var/lib/kubelet/kubelet-flags.env /var/lib/kubelet/kubeadm-flags.env; do if [ -f "$FILE" ]; then sed -i -E 's#(^|[[:space:]"])-endpoint=#\1--container-runtime-endpoint=#g; s#(^|[[:space:]"])--container-runtime(=[^[:space:]"]*)?([[:space:]"]|$)#\1\3#g; s#(^|[[:space:]"])--pod-infra-container-image(=[^[:space:]"]*)?([[:space:]"]|$)#\1\3#g' "$FILE"; fi; done`,
|
||||
"systemctl daemon-reload",
|
||||
"systemctl restart kubelet",
|
||||
)
|
||||
|
||||
@@ -0,0 +1,675 @@
|
||||
// Copyright © 2022 sealos.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/dynamic"
|
||||
kubernetesclient "k8s.io/client-go/kubernetes"
|
||||
clientset "k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
|
||||
ckubeadm "k8s.io/kubernetes/cmd/kubeadm/app/constants"
|
||||
|
||||
clientkubernetes "github.com/labring/sealos/pkg/client-go/kubernetes"
|
||||
"github.com/labring/sealos/pkg/constants"
|
||||
"github.com/labring/sealos/pkg/runtime/kubernetes/types"
|
||||
v1beta1 "github.com/labring/sealos/pkg/types/v1beta1"
|
||||
)
|
||||
|
||||
func TestSanitizeKubeletConfigForPre130(t *testing.T) {
|
||||
kubeletConfig := []byte(`apiVersion: kubelet.config.k8s.io/v1beta1
|
||||
kind: KubeletConfiguration
|
||||
containerRuntimeEndpoint: unix:///run/containerd/containerd.sock
|
||||
imageMaximumGCAge: 0s
|
||||
logging:
|
||||
format: text
|
||||
options:
|
||||
json:
|
||||
infoBufferSize: "0"
|
||||
text:
|
||||
infoBufferSize: "0"
|
||||
`)
|
||||
|
||||
got, err := sanitizeKubeletConfigForVersion(kubeletConfig, "v1.27.1")
|
||||
if err != nil {
|
||||
t.Fatalf("sanitize kubelet config: %v", err)
|
||||
}
|
||||
|
||||
for _, field := range []string{"containerRuntimeEndpoint", "imageMaximumGCAge"} {
|
||||
if strings.Contains(string(got), field) {
|
||||
t.Fatalf("expected %q to be removed from kubelet config:\n%s", field, got)
|
||||
}
|
||||
}
|
||||
if strings.Contains(string(got), "\n text:") {
|
||||
t.Fatalf("expected logging.options.text to be removed from kubelet config:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(string(got), "format: text") {
|
||||
t.Fatalf("expected logging.format to be preserved:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(string(got), "json:") {
|
||||
t.Fatalf("expected logging.options.json to be preserved:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeKubeletConfigFor130KeepsFields(t *testing.T) {
|
||||
kubeletConfig := []byte(`apiVersion: kubelet.config.k8s.io/v1beta1
|
||||
kind: KubeletConfiguration
|
||||
containerRuntimeEndpoint: unix:///run/containerd/containerd.sock
|
||||
imageMaximumGCAge: 0s
|
||||
logging:
|
||||
options:
|
||||
text:
|
||||
infoBufferSize: "0"
|
||||
`)
|
||||
|
||||
got, err := sanitizeKubeletConfigForVersion(kubeletConfig, "v1.30.0")
|
||||
if err != nil {
|
||||
t.Fatalf("sanitize kubelet config: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, kubeletConfig) {
|
||||
t.Fatalf("expected kubelet config for v1.30.0 to be unchanged:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpgradeApplyCommandIgnoresHealthCheckJob(t *testing.T) {
|
||||
got, err := getUpgradeApplyCmd("v1.30.9")
|
||||
if err != nil {
|
||||
t.Fatalf("getUpgradeApplyCmd() error = %v", err)
|
||||
}
|
||||
for _, want := range []string{
|
||||
"--ignore-preflight-errors=",
|
||||
"SystemVerification",
|
||||
"ControlPlaneNodesReady",
|
||||
"CreateJob",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("upgrade apply command %q does not contain %q", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUpgradeCommandsFollowCertificateRenewalPolicy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
version string
|
||||
wantApplyDisableRenew bool
|
||||
wantNodeDisableRenew bool
|
||||
}{
|
||||
{
|
||||
name: "keep disable-renewal before v131",
|
||||
version: "v1.30.9",
|
||||
wantApplyDisableRenew: true,
|
||||
wantNodeDisableRenew: true,
|
||||
},
|
||||
{
|
||||
name: "enable kubeadm renewal on v131 and later",
|
||||
version: "v1.31.0",
|
||||
wantApplyDisableRenew: false,
|
||||
wantNodeDisableRenew: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
applyCmd, err := getUpgradeApplyCmd(tt.version)
|
||||
if err != nil {
|
||||
t.Fatalf("getUpgradeApplyCmd() error = %v", err)
|
||||
}
|
||||
if got := strings.Contains(applyCmd, "--certificate-renewal=false"); got != tt.wantApplyDisableRenew {
|
||||
t.Fatalf("getUpgradeApplyCmd() disable-renewal = %v, want %v, cmd = %q", got, tt.wantApplyDisableRenew, applyCmd)
|
||||
}
|
||||
|
||||
nodeCmd, err := getUpgradeNodeCmd(tt.version)
|
||||
if err != nil {
|
||||
t.Fatalf("getUpgradeNodeCmd() error = %v", err)
|
||||
}
|
||||
if got := strings.Contains(nodeCmd, "--certificate-renewal=false"); got != tt.wantNodeDisableRenew {
|
||||
t.Fatalf("getUpgradeNodeCmd() disable-renewal = %v, want %v, cmd = %q", got, tt.wantNodeDisableRenew, nodeCmd)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetterKubeadmAPIVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
version string
|
||||
want string
|
||||
}{
|
||||
{version: "v1.30.9", want: types.KubeadmV1beta3},
|
||||
{version: "v1.31.0", want: types.KubeadmV1beta4},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if got := getterKubeadmAPIVersion(tt.version); got != tt.want {
|
||||
t.Fatalf("getterKubeadmAPIVersion(%q) = %q, want %q", tt.version, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalConfigsForVersionAddsCertValidityPeriodsForV131(t *testing.T) {
|
||||
clusterConfig := map[string]interface{}{
|
||||
"apiVersion": types.KubeadmV1beta4,
|
||||
"kind": "ClusterConfiguration",
|
||||
}
|
||||
|
||||
got, err := marshalConfigsForVersion("v1.31.0", clusterConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("marshalConfigsForVersion() error = %v", err)
|
||||
}
|
||||
|
||||
for _, want := range []string{
|
||||
"certificateValidityPeriod: 876000h",
|
||||
"caCertificateValidityPeriod: 876000h",
|
||||
} {
|
||||
if !strings.Contains(string(got), want) {
|
||||
t.Fatalf("marshalConfigsForVersion() output %q does not contain %q", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalConfigsForVersionSkipsCertValidityPeriodsBeforeV131(t *testing.T) {
|
||||
clusterConfig := map[string]interface{}{
|
||||
"apiVersion": types.KubeadmV1beta3,
|
||||
"kind": "ClusterConfiguration",
|
||||
}
|
||||
|
||||
got, err := marshalConfigsForVersion("v1.30.9", clusterConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("marshalConfigsForVersion() error = %v", err)
|
||||
}
|
||||
if strings.Contains(string(got), "certificateValidityPeriod:") {
|
||||
t.Fatalf("expected pre-v1.31 config to skip certificate validity periods, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldMigrateAPIServerKubeletClientCert(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
currentVersion string
|
||||
targetVersion string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "migrate across 129 boundary",
|
||||
currentVersion: "v1.28.9",
|
||||
targetVersion: "v1.29.0",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "skip same minor after 129",
|
||||
currentVersion: "v1.29.1",
|
||||
targetVersion: "v1.29.2",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "skip below 129",
|
||||
currentVersion: "v1.28.1",
|
||||
targetVersion: "v1.28.9",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "migrate when upgrading past 129",
|
||||
currentVersion: "v1.28.10",
|
||||
targetVersion: "v1.30.1",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := shouldMigrateAPIServerKubeletClientCert(tt.currentVersion, tt.targetVersion)
|
||||
if err != nil {
|
||||
t.Fatalf("shouldMigrateAPIServerKubeletClientCert() error = %v", err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("shouldMigrateAPIServerKubeletClientCert() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldMigrateAPIServerKubeletClientCertRejectsInvalidVersions(t *testing.T) {
|
||||
if _, err := shouldMigrateAPIServerKubeletClientCert("invalid", "v1.29.0"); err == nil {
|
||||
t.Fatal("expected invalid current version to return an error")
|
||||
}
|
||||
if _, err := shouldMigrateAPIServerKubeletClientCert("v1.28.9", "invalid"); err == nil {
|
||||
t.Fatal("expected invalid target version to return an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRemoteCertMigrations(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
current string
|
||||
target string
|
||||
hasLocalEtcd bool
|
||||
wantNames []string
|
||||
}{
|
||||
{
|
||||
name: "skip when target stays below 129",
|
||||
current: "v1.28.9",
|
||||
target: "v1.28.10",
|
||||
hasLocalEtcd: true,
|
||||
wantNames: nil,
|
||||
},
|
||||
{
|
||||
name: "migrate kubelet client only for external etcd",
|
||||
current: "v1.28.9",
|
||||
target: "v1.29.0",
|
||||
hasLocalEtcd: false,
|
||||
wantNames: []string{apiserverKubeletClientCertName},
|
||||
},
|
||||
{
|
||||
name: "migrate kubelet and etcd clients for local etcd",
|
||||
current: "v1.28.9",
|
||||
target: "v1.29.0",
|
||||
hasLocalEtcd: true,
|
||||
wantNames: []string{
|
||||
apiserverKubeletClientCertName,
|
||||
apiserverEtcdClientCertName,
|
||||
etcdHealthcheckClientCertName,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := getRemoteCertMigrations(tt.current, tt.target, tt.hasLocalEtcd)
|
||||
if err != nil {
|
||||
t.Fatalf("getRemoteCertMigrations() error = %v", err)
|
||||
}
|
||||
if len(got) != len(tt.wantNames) {
|
||||
t.Fatalf("getRemoteCertMigrations() len = %d, want %d (%v)", len(got), len(tt.wantNames), tt.wantNames)
|
||||
}
|
||||
for i := range got {
|
||||
if got[i].name != tt.wantNames[i] {
|
||||
t.Fatalf("getRemoteCertMigrations()[%d] = %q, want %q", i, got[i].name, tt.wantNames[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoUpdateConfigTreatsDefaultEtcdAsLocal(t *testing.T) {
|
||||
rootDir := t.TempDir()
|
||||
prevRuntimeRoot := constants.DefaultRuntimeRootDir
|
||||
constants.DefaultRuntimeRootDir = rootDir
|
||||
t.Cleanup(func() {
|
||||
constants.DefaultRuntimeRootDir = prevRuntimeRoot
|
||||
})
|
||||
|
||||
clusterConfig := `apiVersion: kubeadm.k8s.io/v1beta3
|
||||
kind: ClusterConfiguration
|
||||
kubernetesVersion: v1.28.9
|
||||
networking:
|
||||
serviceSubnet: 10.96.0.0/12
|
||||
podSubnet: 10.244.0.0/16
|
||||
dnsDomain: cluster.local
|
||||
apiServer:
|
||||
certSANs:
|
||||
- 127.0.0.1`
|
||||
kubeletConfig := `apiVersion: kubelet.config.k8s.io/v1beta1
|
||||
kind: KubeletConfiguration
|
||||
cgroupDriver: systemd`
|
||||
|
||||
client := clientset.NewSimpleClientset(
|
||||
&corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: ckubeadm.KubeadmConfigConfigMap,
|
||||
Namespace: metav1.NamespaceSystem,
|
||||
},
|
||||
Data: map[string]string{
|
||||
ckubeadm.ClusterConfigurationConfigMapKey: clusterConfig,
|
||||
},
|
||||
},
|
||||
&corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: ckubeadm.KubeletBaseConfigurationConfigMap,
|
||||
Namespace: metav1.NamespaceSystem,
|
||||
},
|
||||
Data: map[string]string{
|
||||
ckubeadm.KubeletBaseConfigurationConfigMapKey: kubeletConfig,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
cfgDir := filepath.Join(rootDir, "test-cluster", "etc")
|
||||
if err := os.MkdirAll(cfgDir, 0o755); err != nil {
|
||||
t.Fatalf("mkdir etc dir: %v", err)
|
||||
}
|
||||
adminFile := filepath.Join(cfgDir, "admin.conf")
|
||||
if err := os.WriteFile(adminFile, []byte("apiVersion: v1\nkind: Config\n"), 0o600); err != nil {
|
||||
t.Fatalf("write admin.conf: %v", err)
|
||||
}
|
||||
|
||||
rt := &KubeadmRuntime{
|
||||
cli: &stubKubeClient{
|
||||
k8s: client,
|
||||
cfg: &rest.Config{Host: "https://127.0.0.1:6443"},
|
||||
},
|
||||
cluster: &v1beta1.Cluster{},
|
||||
kubeadmConfig: types.NewKubeadmConfig(),
|
||||
pathResolver: constants.NewPathResolver("test-cluster"),
|
||||
}
|
||||
|
||||
_, hasLocalEtcd, err := rt.autoUpdateConfig("v1.29.15")
|
||||
if err != nil {
|
||||
t.Fatalf("autoUpdateConfig() error = %v", err)
|
||||
}
|
||||
if !hasLocalEtcd {
|
||||
t.Fatal("expected kubeadm default etcd config without external etcd to be treated as local etcd")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUsesLocalEtcd(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
etcd kubeadm.Etcd
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "default kubeadm etcd",
|
||||
etcd: kubeadm.Etcd{},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "explicit local etcd",
|
||||
etcd: kubeadm.Etcd{Local: &kubeadm.LocalEtcd{}},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "external etcd with endpoints",
|
||||
etcd: kubeadm.Etcd{External: &kubeadm.ExternalEtcd{Endpoints: []string{"https://127.0.0.1:2379"}}},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty external etcd falls back to local kubeadm behavior",
|
||||
etcd: kubeadm.Etcd{External: &kubeadm.ExternalEtcd{}},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := usesLocalEtcd(tt.etcd); got != tt.want {
|
||||
t.Fatalf("usesLocalEtcd() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldRegenerateRemoteAdminKubeConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
version string
|
||||
want bool
|
||||
}{
|
||||
{version: "v1.28.9", want: false},
|
||||
{version: "v1.29.0", want: true},
|
||||
{version: "v1.31.1", want: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if got := shouldRegenerateRemoteAdminKubeConfig(tt.version); got != tt.want {
|
||||
t.Fatalf("shouldRegenerateRemoteAdminKubeConfig(%q) = %v, want %v", tt.version, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRegenerateRemoteAdminKubeConfigCmd(t *testing.T) {
|
||||
cmd := buildRegenerateRemoteAdminKubeConfigCmd("/tmp/kubeadm-migration.yaml")
|
||||
for _, want := range []string{
|
||||
"rm -f /etc/kubernetes/admin.conf",
|
||||
"kubeadm init phase kubeconfig admin --config /tmp/kubeadm-migration.yaml",
|
||||
"mv -f \"$backup_dir/admin.conf\" /etc/kubernetes/admin.conf",
|
||||
} {
|
||||
if !strings.Contains(cmd, want) {
|
||||
t.Fatalf("buildRegenerateRemoteAdminKubeConfigCmd() output %q does not contain %q", cmd, want)
|
||||
}
|
||||
}
|
||||
if strings.Contains(cmd, "kubeadm certs renew admin.conf") {
|
||||
t.Fatalf("buildRegenerateRemoteAdminKubeConfigCmd() should not use renew, got %q", cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncUpgradeMigrationConfigCopiesToAllTargets(t *testing.T) {
|
||||
rootDir := t.TempDir()
|
||||
prevRuntimeRoot := constants.DefaultRuntimeRootDir
|
||||
constants.DefaultRuntimeRootDir = rootDir
|
||||
t.Cleanup(func() {
|
||||
constants.DefaultRuntimeRootDir = prevRuntimeRoot
|
||||
})
|
||||
|
||||
rt := &KubeadmRuntime{
|
||||
execer: &stubSSH{},
|
||||
cluster: testCluster([]string{"master0", "master1", "master2"}),
|
||||
pathResolver: constants.NewPathResolver("test-cluster"),
|
||||
}
|
||||
|
||||
localConfigPath := rt.upgradeMigrationConfigLocalPath()
|
||||
if err := os.MkdirAll(filepath.Dir(localConfigPath), 0o755); err != nil {
|
||||
t.Fatalf("mkdir migration dir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(localConfigPath, []byte("kind: ClusterConfiguration\n"), 0o600); err != nil {
|
||||
t.Fatalf("write migration config: %v", err)
|
||||
}
|
||||
|
||||
stub := rt.execer.(*stubSSH)
|
||||
if err := rt.syncUpgradeMigrationConfig([]string{"master1", "master2"}); err != nil {
|
||||
t.Fatalf("syncUpgradeMigrationConfig() error = %v", err)
|
||||
}
|
||||
|
||||
wantCopies := []string{
|
||||
"master1|" + localConfigPath + "|" + rt.remoteUpgradeMigrationConfigPath(),
|
||||
"master2|" + localConfigPath + "|" + rt.remoteUpgradeMigrationConfigPath(),
|
||||
}
|
||||
gotCopies := append([]string{}, stub.copyCalls...)
|
||||
sort.Strings(gotCopies)
|
||||
sort.Strings(wantCopies)
|
||||
if len(gotCopies) != len(wantCopies) {
|
||||
t.Fatalf("syncUpgradeMigrationConfig() copyCalls = %v, want %v", gotCopies, wantCopies)
|
||||
}
|
||||
for i := range wantCopies {
|
||||
if gotCopies[i] != wantCopies[i] {
|
||||
t.Fatalf("syncUpgradeMigrationConfig() copyCalls = %v, want %v", gotCopies, wantCopies)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreV131RemoteIdentityMigrationsAvoidKubeadmPhaseRegeneration(t *testing.T) {
|
||||
applyCmd, err := getUpgradeApplyCmd("v1.29.15")
|
||||
if err != nil {
|
||||
t.Fatalf("getUpgradeApplyCmd() error = %v", err)
|
||||
}
|
||||
if !strings.Contains(applyCmd, "--certificate-renewal=false") {
|
||||
t.Fatalf("expected pre-v1.31 upgrade apply to keep certificate renewal disabled, got %q", applyCmd)
|
||||
}
|
||||
|
||||
if got := shouldRegenerateRemoteAdminKubeConfig("v1.29.15"); !got {
|
||||
t.Fatal("expected remote admin.conf regeneration to remain enabled for v1.29.15")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStagedLocalPKIFileUsesRegeneratedLocalFiles(t *testing.T) {
|
||||
stagingDir := t.TempDir()
|
||||
files := []string{
|
||||
"apiserver-etcd-client.crt",
|
||||
"etcd/healthcheck-client.crt",
|
||||
}
|
||||
for _, fileName := range files {
|
||||
filePath := filepath.Join(stagingDir, filepath.FromSlash(fileName))
|
||||
if err := os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil {
|
||||
t.Fatalf("mkdir %s: %v", filepath.Dir(filePath), err)
|
||||
}
|
||||
if err := os.WriteFile(filePath, []byte("cert"), 0o600); err != nil {
|
||||
t.Fatalf("write %s: %v", filePath, err)
|
||||
}
|
||||
got, err := stagedLocalPKIFile(stagingDir, fileName)
|
||||
if err != nil {
|
||||
t.Fatalf("stagedLocalPKIFile(%q) error = %v", fileName, err)
|
||||
}
|
||||
if got != filePath {
|
||||
t.Fatalf("stagedLocalPKIFile(%q) = %q, want %q", fileName, got, filePath)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := stagedLocalPKIFile(stagingDir, "missing.crt"); err == nil {
|
||||
t.Fatal("expected missing staged file to return an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeWithBuiltinKubeadmConfigLoadsNetworkingFromClusterConfig(t *testing.T) {
|
||||
rootDir := t.TempDir()
|
||||
prevRuntimeRoot := constants.DefaultRuntimeRootDir
|
||||
constants.DefaultRuntimeRootDir = rootDir
|
||||
t.Cleanup(func() {
|
||||
constants.DefaultRuntimeRootDir = prevRuntimeRoot
|
||||
})
|
||||
|
||||
clusterConfig := `apiVersion: kubeadm.k8s.io/v1beta3
|
||||
kind: ClusterConfiguration
|
||||
networking:
|
||||
serviceSubnet: 10.96.0.0/12
|
||||
podSubnet: 10.244.0.0/16
|
||||
dnsDomain: cluster.local
|
||||
apiServer:
|
||||
certSANs:
|
||||
- 127.0.0.1
|
||||
- apiserver.example.local`
|
||||
|
||||
client := clientset.NewSimpleClientset(&corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: ckubeadm.KubeadmConfigConfigMap,
|
||||
Namespace: metav1.NamespaceSystem,
|
||||
},
|
||||
Data: map[string]string{
|
||||
ckubeadm.ClusterConfigurationConfigMapKey: clusterConfig,
|
||||
},
|
||||
})
|
||||
|
||||
cfgDir := filepath.Join(rootDir, "test-cluster", "etc")
|
||||
if err := os.MkdirAll(cfgDir, 0o755); err != nil {
|
||||
t.Fatalf("mkdir etc dir: %v", err)
|
||||
}
|
||||
adminFile := filepath.Join(cfgDir, "admin.conf")
|
||||
if err := os.WriteFile(adminFile, []byte("apiVersion: v1\nkind: Config\n"), 0o600); err != nil {
|
||||
t.Fatalf("write admin.conf: %v", err)
|
||||
}
|
||||
|
||||
rt := &KubeadmRuntime{
|
||||
cli: &stubKubeClient{
|
||||
k8s: client,
|
||||
cfg: &rest.Config{Host: "https://127.0.0.1:6443"},
|
||||
},
|
||||
cluster: &v1beta1.Cluster{},
|
||||
kubeadmConfig: types.NewKubeadmConfig(),
|
||||
pathResolver: constants.NewPathResolver("test-cluster"),
|
||||
}
|
||||
rt.kubeadmConfig.ClusterConfiguration.Networking.ServiceSubnet = ""
|
||||
|
||||
if err := rt.mergeWithBuiltinKubeadmConfig(); err != nil {
|
||||
t.Fatalf("mergeWithBuiltinKubeadmConfig() error = %v", err)
|
||||
}
|
||||
if got := rt.getServiceCIDR(); got != "10.96.0.0/12" {
|
||||
t.Fatalf("serviceSubnet = %q, want %q", got, "10.96.0.0/12")
|
||||
}
|
||||
if got := rt.getDNSDomain(); got != "cluster.local" {
|
||||
t.Fatalf("dnsDomain = %q, want %q", got, "cluster.local")
|
||||
}
|
||||
if got := rt.getCertSANs(); len(got) != 2 || got[1] != "apiserver.example.local" {
|
||||
t.Fatalf("certSANs = %v, want [127.0.0.1 apiserver.example.local]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreV131RemoteMigrationLoadsNetworkingFromClusterConfig(t *testing.T) {
|
||||
rootDir := t.TempDir()
|
||||
prevRuntimeRoot := constants.DefaultRuntimeRootDir
|
||||
constants.DefaultRuntimeRootDir = rootDir
|
||||
t.Cleanup(func() {
|
||||
constants.DefaultRuntimeRootDir = prevRuntimeRoot
|
||||
})
|
||||
|
||||
clusterConfig := `apiVersion: kubeadm.k8s.io/v1beta3
|
||||
kind: ClusterConfiguration
|
||||
networking:
|
||||
serviceSubnet: 10.96.0.0/12
|
||||
podSubnet: 10.244.0.0/16
|
||||
dnsDomain: cluster.local
|
||||
apiServer:
|
||||
certSANs:
|
||||
- 127.0.0.1
|
||||
- apiserver.example.local`
|
||||
|
||||
client := clientset.NewSimpleClientset(&corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: ckubeadm.KubeadmConfigConfigMap,
|
||||
Namespace: metav1.NamespaceSystem,
|
||||
},
|
||||
Data: map[string]string{
|
||||
ckubeadm.ClusterConfigurationConfigMapKey: clusterConfig,
|
||||
},
|
||||
})
|
||||
|
||||
cfgDir := filepath.Join(rootDir, "test-cluster", "etc")
|
||||
if err := os.MkdirAll(cfgDir, 0o755); err != nil {
|
||||
t.Fatalf("mkdir etc dir: %v", err)
|
||||
}
|
||||
adminFile := filepath.Join(cfgDir, "admin.conf")
|
||||
if err := os.WriteFile(adminFile, []byte("apiVersion: v1\nkind: Config\n"), 0o600); err != nil {
|
||||
t.Fatalf("write admin.conf: %v", err)
|
||||
}
|
||||
|
||||
rt := &KubeadmRuntime{
|
||||
cli: &stubKubeClient{
|
||||
k8s: client,
|
||||
cfg: &rest.Config{Host: "https://127.0.0.1:6443"},
|
||||
},
|
||||
cluster: &v1beta1.Cluster{},
|
||||
kubeadmConfig: types.NewKubeadmConfig(),
|
||||
pathResolver: constants.NewPathResolver("test-cluster"),
|
||||
}
|
||||
rt.kubeadmConfig.ClusterConfiguration.Networking.ServiceSubnet = ""
|
||||
|
||||
if err := rt.mergeWithBuiltinKubeadmConfig(); err != nil {
|
||||
t.Fatalf("mergeWithBuiltinKubeadmConfig() error = %v", err)
|
||||
}
|
||||
if got := rt.getServiceCIDR(); got != "10.96.0.0/12" {
|
||||
t.Fatalf("serviceSubnet = %q, want %q", got, "10.96.0.0/12")
|
||||
}
|
||||
}
|
||||
|
||||
type stubKubeClient struct {
|
||||
k8s kubernetesclient.Interface
|
||||
cfg *rest.Config
|
||||
}
|
||||
|
||||
var _ clientkubernetes.Client = (*stubKubeClient)(nil)
|
||||
|
||||
func (s *stubKubeClient) Kubernetes() kubernetesclient.Interface { return s.k8s }
|
||||
|
||||
func (s *stubKubeClient) Discovery() discovery.DiscoveryInterface { return nil }
|
||||
|
||||
func (s *stubKubeClient) KubernetesDynamic() dynamic.Interface { return nil }
|
||||
|
||||
func (s *stubKubeClient) Config() *rest.Config { return s.cfg }
|
||||
@@ -24,6 +24,11 @@ ifeq ($(DEBUG), 1)
|
||||
endif
|
||||
GO_BUILD_FLAGS += -tags "containers_image_openpgp netgo exclude_graphdriver_devicemapper static osusergo exclude_graphdriver_btrfs" -trimpath -ldflags "$(GO_LDFLAGS)"
|
||||
|
||||
USER_CC_OVERRIDE := $(if $(filter-out default undefined,$(origin CC)),$(value CC),)
|
||||
define platform_cc_override
|
||||
$(if $(filter-out undefined,$(origin CC_$(1))),$(value CC_$(1)),$(USER_CC_OVERRIDE))
|
||||
endef
|
||||
|
||||
ifeq ($(ROOT_PACKAGE),)
|
||||
$(error the variable ROOT_PACKAGE must be set prior to including golang.mk)
|
||||
endif
|
||||
@@ -62,11 +67,41 @@ go.build.%:
|
||||
|
||||
@if [ "$(COMMAND)" == "sealos" ] || [ "$(COMMAND)" == "sealctl" ]; then \
|
||||
CGO_ENABLED=1; \
|
||||
CC=x86_64-linux-gnu-gcc; \
|
||||
if [ "$(ARCH)" == "arm64" ]; then \
|
||||
CC=aarch64-linux-gnu-gcc; \
|
||||
TARGET_CC="$(call platform_cc_override,$(PLATFORM))"; \
|
||||
resolve_cc() { \
|
||||
local candidate cc_bin; \
|
||||
for candidate in "$$@"; do \
|
||||
if [ -z "$$candidate" ]; then \
|
||||
continue; \
|
||||
fi; \
|
||||
cc_bin="$${candidate%% *}"; \
|
||||
if command -v "$$cc_bin" >/dev/null 2>&1; then \
|
||||
printf '%s\n' "$$candidate"; \
|
||||
return 0; \
|
||||
fi; \
|
||||
done; \
|
||||
return 1; \
|
||||
}; \
|
||||
if [ -z "$$TARGET_CC" ]; then \
|
||||
HOST_OS="$$($(GO) env GOOS)"; \
|
||||
HOST_ARCH="$$($(GO) env GOARCH)"; \
|
||||
HOST_CC="$$($(GO) env CC)"; \
|
||||
if [ "$(OS)" == "$$HOST_OS" ] && [ "$(ARCH)" == "$$HOST_ARCH" ]; then \
|
||||
TARGET_CC="$$(resolve_cc "$$HOST_CC" cc gcc clang)"; \
|
||||
else \
|
||||
case "$(ARCH)" in \
|
||||
amd64) GNU_ARCH=x86_64 ;; \
|
||||
arm64) GNU_ARCH=aarch64 ;; \
|
||||
*) GNU_ARCH="$(ARCH)" ;; \
|
||||
esac; \
|
||||
TARGET_CC="$$(resolve_cc "$$GNU_ARCH-$(OS)-gnu-gcc" "$$GNU_ARCH-pc-$(OS)-gnu-gcc")"; \
|
||||
fi; \
|
||||
fi; \
|
||||
CGO_ENABLED=$$CGO_ENABLED CC=$$CC GOOS=$(OS) GOARCH=$(ARCH) $(GO) build $(GO_BUILD_FLAGS) -o $(BIN_DIR)/$(PLATFORM)/$(COMMAND) $(ROOT_PACKAGE)/cmd/$(COMMAND); \
|
||||
if [ -z "$$TARGET_CC" ]; then \
|
||||
echo "No usable C compiler found for $(PLATFORM). Set CC or CC_$(PLATFORM) explicitly."; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
CGO_ENABLED=$$CGO_ENABLED CC="$$TARGET_CC" GOOS=$(OS) GOARCH=$(ARCH) $(GO) build $(GO_BUILD_FLAGS) -o $(BIN_DIR)/$(PLATFORM)/$(COMMAND) $(ROOT_PACKAGE)/cmd/$(COMMAND); \
|
||||
else \
|
||||
CGO_ENABLED=0 GOOS=$(OS) GOARCH=$(ARCH) $(GO) build $(GO_BUILD_FLAGS) -o $(BIN_DIR)/$(PLATFORM)/$(COMMAND) $(ROOT_PACKAGE)/cmd/$(COMMAND); \
|
||||
fi
|
||||
@@ -102,4 +137,3 @@ go.format: tools.verify.goimports
|
||||
.PHONY: go.coverage
|
||||
go.coverage:
|
||||
@$(GO) test -race -failfast -coverprofile=coverage.out -covermode=atomic `go list ./pkg/env ./pkg/apply | grep -v "/test\|/fork"`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user