github.com/sealerio/sealer@v0.11.1-0.20240507115618-f4f89c5853ae/pkg/registry/local.go (about)

     1  // Copyright © 2022 Alibaba Group Holding Ltd.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package registry
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"encoding/json"
    21  	"fmt"
    22  	"net"
    23  	"os"
    24  	"path"
    25  	"path/filepath"
    26  	"sort"
    27  	"strconv"
    28  	"strings"
    29  
    30  	"github.com/sealerio/sealer/common"
    31  	containerruntime "github.com/sealerio/sealer/pkg/container-runtime"
    32  	"github.com/sealerio/sealer/pkg/imagedistributor"
    33  	"github.com/sealerio/sealer/pkg/infradriver"
    34  	"github.com/sealerio/sealer/pkg/ipvs"
    35  	v2 "github.com/sealerio/sealer/types/api/v2"
    36  	netutils "github.com/sealerio/sealer/utils/net"
    37  	osutils "github.com/sealerio/sealer/utils/os"
    38  	"github.com/sealerio/sealer/utils/shellcommand"
    39  
    40  	"github.com/containers/common/pkg/auth"
    41  	"github.com/pelletier/go-toml"
    42  	"github.com/sirupsen/logrus"
    43  	"golang.org/x/sync/errgroup"
    44  	k8snet "k8s.io/utils/net"
    45  )
    46  
    47  const (
    48  	LvscarePodFileName = "reg-lvscare.yaml"
    49  )
    50  
    51  type localConfigurator struct {
    52  	*v2.LocalRegistry
    53  	deployHosts          []net.IP
    54  	containerRuntimeInfo containerruntime.Info
    55  	infraDriver          infradriver.InfraDriver
    56  	distributor          imagedistributor.Distributor
    57  }
    58  
    59  func (c *localConfigurator) GetRegistryInfo() RegistryInfo {
    60  	registryInfo := RegistryInfo{Local: LocalRegistryInfo{LocalRegistry: c.LocalRegistry}}
    61  	if *c.LocalRegistry.HA {
    62  		registryInfo.Local.Vip = GetRegistryVIP(c.infraDriver)
    63  		registryInfo.Local.DeployHosts = c.deployHosts
    64  	} else {
    65  		registryInfo.Local.DeployHosts = append(registryInfo.Local.DeployHosts, c.deployHosts[0])
    66  	}
    67  	return registryInfo
    68  }
    69  
    70  func (c *localConfigurator) GetDriver() (Driver, error) {
    71  	endpoint := net.JoinHostPort(c.Domain, strconv.Itoa(c.Port))
    72  	dataDir := filepath.Join(c.infraDriver.GetClusterRootfsPath(), "registry")
    73  	return newLocalRegistryDriver(endpoint, dataDir, c.deployHosts, c.distributor), nil
    74  }
    75  
    76  func (c *localConfigurator) UninstallFrom(deletedMasters, deletedNodes []net.IP) error {
    77  	// remove all registry configs on /etc/hosts
    78  	all := append(deletedMasters, deletedNodes...)
    79  	if err := c.removeRegistryConfig(all); err != nil {
    80  		return err
    81  	}
    82  	if !*c.HA {
    83  		return nil
    84  	}
    85  	// if current deployHosts is null,means clean all, just return.
    86  	if len(c.deployHosts) == 0 {
    87  		return nil
    88  	}
    89  	// if deletedMasters is nil, means no need to flush workers, just return
    90  	if len(deletedMasters) == 0 {
    91  		return nil
    92  	}
    93  	// flush ipvs policy on remain nodes
    94  	return c.configureLvs(c.deployHosts, netutils.RemoveIPs(c.infraDriver.GetHostIPListByRole(common.NODE), deletedNodes))
    95  }
    96  
    97  func (c *localConfigurator) removeRegistryConfig(hosts []net.IP) error {
    98  	var uninstallCmd []string
    99  	if c.RegistryConfig.Username != "" && c.RegistryConfig.Password != "" {
   100  		//todo use sdk to logout instead of shell cmd
   101  		logoutCmd := fmt.Sprintf("docker logout %s ", net.JoinHostPort(c.Domain, strconv.Itoa(c.Port)))
   102  		if c.containerRuntimeInfo.Type != common.Docker {
   103  			logoutCmd = fmt.Sprintf("nerdctl logout %s ", net.JoinHostPort(c.Domain, strconv.Itoa(c.Port)))
   104  		}
   105  		uninstallCmd = append(uninstallCmd, logoutCmd)
   106  	}
   107  
   108  	f := func(host net.IP) error {
   109  		err := c.infraDriver.CmdAsync(host, nil, strings.Join(uninstallCmd, "&&"))
   110  		if err != nil {
   111  			return fmt.Errorf("failed to delete registry configuration: %v", err)
   112  		}
   113  		return nil
   114  	}
   115  
   116  	return c.infraDriver.Execute(hosts, f)
   117  }
   118  
   119  func (c *localConfigurator) InstallOn(masters, nodes []net.IP) error {
   120  	hosts := append(masters, nodes...)
   121  	logrus.Infof("will install local private registry configuration on %+v\n", hosts)
   122  	if err := c.configureRegistryNetwork(masters, nodes); err != nil {
   123  		return err
   124  	}
   125  
   126  	if err := c.configureRegistryCert(hosts); err != nil {
   127  		return err
   128  	}
   129  
   130  	if err := c.configureDaemonService(hosts); err != nil {
   131  		return err
   132  	}
   133  
   134  	if err := c.configureAccessCredential(hosts); err != nil {
   135  		return err
   136  	}
   137  
   138  	return nil
   139  }
   140  
   141  // add registry domain and ip to "/etc/hosts"
   142  // add registry ip to ipvs policy
   143  func (c *localConfigurator) configureRegistryNetwork(masters, nodes []net.IP) error {
   144  	if !*c.HA {
   145  		return c.configureSingletonHostsFile(append(masters, nodes...))
   146  	}
   147  
   148  	eg, _ := errgroup.WithContext(context.Background())
   149  
   150  	for i := range masters {
   151  		master := masters[i]
   152  		eg.Go(func() error {
   153  			cmd := shellcommand.CommandSetHostAlias(c.Domain, master.String())
   154  			if err := c.infraDriver.CmdAsync(master, nil, cmd); err != nil {
   155  				return fmt.Errorf("failed to config masters hosts file: %v", err)
   156  			}
   157  			return nil
   158  		})
   159  	}
   160  
   161  	if err := eg.Wait(); err != nil {
   162  		return err
   163  	}
   164  
   165  	// if masters is nil, means no need to flush old nodes
   166  	if len(masters) == 0 {
   167  		return c.configureLvs(c.deployHosts, nodes)
   168  	}
   169  	return c.configureLvs(c.deployHosts, c.infraDriver.GetHostIPListByRole(common.NODE))
   170  }
   171  
   172  func (c *localConfigurator) configureLvs(registryHosts, clientHosts []net.IP) error {
   173  	var rs []string
   174  	var realEndpoints []string
   175  	hosts := netutils.IPsToIPStrs(registryHosts)
   176  	sort.Strings(hosts)
   177  	for _, m := range hosts {
   178  		ep := net.JoinHostPort(m, strconv.Itoa(c.Port))
   179  		rs = append(rs, fmt.Sprintf("--rs %s", ep))
   180  		realEndpoints = append(realEndpoints, ep)
   181  	}
   182  
   183  	//todo should make lvs image name as const value in sealer repo.
   184  	lvsImageURL := path.Join(net.JoinHostPort(c.Domain, strconv.Itoa(c.Port)), common.LvsCareRepoAndTag)
   185  
   186  	vip := GetRegistryVIP(c.infraDriver)
   187  
   188  	vs := net.JoinHostPort(vip, strconv.Itoa(c.Port))
   189  	// due to registry server do not have health path to check, choose "/" as default.
   190  	healthPath := "/"
   191  	healthSchem := "https"
   192  	if *c.Insecure {
   193  		healthSchem = "http"
   194  	}
   195  
   196  	y, err := ipvs.LvsStaticPodYaml(common.RegLvsCareStaticPodName, vs, realEndpoints, lvsImageURL, healthPath, healthSchem)
   197  	if err != nil {
   198  		return err
   199  	}
   200  
   201  	lvscareStaticCmd := ipvs.GetCreateLvscareStaticPodCmd(y, LvscarePodFileName)
   202  
   203  	ipvsCmd := fmt.Sprintf("seautil ipvs --vs %s %s --health-path %s --health-schem %s --run-once",
   204  		vs, strings.Join(rs, " "), healthPath, healthSchem)
   205  	// flush all cluster nodes as latest ipvs policy.
   206  	eg, _ := errgroup.WithContext(context.Background())
   207  
   208  	for i := range clientHosts {
   209  		n := clientHosts[i]
   210  		eg.Go(func() error {
   211  			err := c.infraDriver.CmdAsync(n, nil, ipvsCmd, lvscareStaticCmd)
   212  			if err != nil {
   213  				return fmt.Errorf("failed to config nodes lvs policy: %s: %v", ipvsCmd, err)
   214  			}
   215  
   216  			err = c.infraDriver.CmdAsync(n, nil, shellcommand.CommandSetHostAlias(c.Domain, vip))
   217  			if err != nil {
   218  				return fmt.Errorf("failed to config nodes hosts file cmd: %v", err)
   219  			}
   220  			return nil
   221  		})
   222  	}
   223  	return eg.Wait()
   224  }
   225  
   226  func (c *localConfigurator) configureSingletonHostsFile(hosts []net.IP) error {
   227  	// add registry ip to "/etc/hosts"
   228  	f := func(host net.IP) error {
   229  		err := c.infraDriver.CmdAsync(host, nil, shellcommand.CommandSetHostAlias(c.Domain, c.deployHosts[0].String()))
   230  		if err != nil {
   231  			return fmt.Errorf("failed to config cluster hosts file cmd: %v", err)
   232  		}
   233  		return nil
   234  	}
   235  
   236  	return c.infraDriver.Execute(hosts, f)
   237  }
   238  
   239  func (c *localConfigurator) configureRegistryCert(hosts []net.IP) error {
   240  	// if deploy registry as InsecureMode ,skip to configure cert.
   241  	if *c.Insecure {
   242  		return nil
   243  	}
   244  
   245  	var (
   246  		endpoint = net.JoinHostPort(c.Domain, strconv.Itoa(c.Port))
   247  		caFile   = c.Domain + ".crt"
   248  		src      = filepath.Join(c.infraDriver.GetClusterRootfsPath(), "certs", caFile)
   249  		dest     = filepath.Join(c.containerRuntimeInfo.CertsDir, endpoint, caFile)
   250  	)
   251  
   252  	return c.copy2RemoteHosts(src, dest, hosts)
   253  }
   254  
   255  func (c *localConfigurator) configureAccessCredential(hosts []net.IP) error {
   256  	var (
   257  		username        = c.RegistryConfig.Username
   258  		password        = c.RegistryConfig.Password
   259  		endpoint        = net.JoinHostPort(c.Domain, strconv.Itoa(c.Port))
   260  		tmpAuthFilePath = "/tmp/config.json"
   261  		// todo we need this config file when kubelet pull images from registry. while, we could optimize the logic here.
   262  		remoteKubeletAuthFilePath = "/var/lib/kubelet/config.json"
   263  	)
   264  
   265  	if username == "" || password == "" {
   266  		return nil
   267  	}
   268  
   269  	err := auth.Login(context.TODO(),
   270  		nil,
   271  		&auth.LoginOptions{
   272  			AuthFile:           tmpAuthFilePath,
   273  			Password:           password,
   274  			Username:           username,
   275  			Stdout:             os.Stdout,
   276  			AcceptRepositories: true,
   277  		},
   278  		[]string{endpoint})
   279  
   280  	if err != nil {
   281  		return err
   282  	}
   283  
   284  	defer func() {
   285  		err = os.Remove(tmpAuthFilePath)
   286  		if err != nil {
   287  			logrus.Debugf("failed to remove tmp registry auth file:%s", tmpAuthFilePath)
   288  		}
   289  	}()
   290  
   291  	err = c.copy2RemoteHosts(tmpAuthFilePath, c.containerRuntimeInfo.ConfigFilePath, hosts)
   292  	if err != nil {
   293  		return err
   294  	}
   295  
   296  	err = c.copy2RemoteHosts(tmpAuthFilePath, remoteKubeletAuthFilePath, hosts)
   297  	if err != nil {
   298  		return err
   299  	}
   300  
   301  	return nil
   302  }
   303  
   304  func (c *localConfigurator) copy2RemoteHosts(src, dest string, hosts []net.IP) error {
   305  	f := func(host net.IP) error {
   306  		err := c.infraDriver.Copy(host, src, dest)
   307  		if err != nil {
   308  			return fmt.Errorf("failed to copy local file %s to remote %s : %v", src, dest, err)
   309  		}
   310  		return nil
   311  	}
   312  
   313  	return c.infraDriver.Execute(hosts, f)
   314  }
   315  
   316  func (c *localConfigurator) configureDaemonService(hosts []net.IP) error {
   317  	var (
   318  		src      string
   319  		dest     string
   320  		endpoint = net.JoinHostPort(c.Domain, strconv.Itoa(c.Port))
   321  	)
   322  
   323  	if endpoint == common.DefaultRegistryURL {
   324  		return nil
   325  	}
   326  
   327  	if c.containerRuntimeInfo.Type == common.Docker {
   328  		src = filepath.Join(c.infraDriver.GetClusterRootfsPath(), "etc", "daemon.json")
   329  		dest = "/etc/docker/daemon.json"
   330  		if err := c.configureDockerDaemonService(endpoint, src); err != nil {
   331  			return err
   332  		}
   333  	}
   334  
   335  	if c.containerRuntimeInfo.Type == common.Containerd {
   336  		src = filepath.Join(c.infraDriver.GetClusterRootfsPath(), "etc", "hosts.toml")
   337  		dest = filepath.Join(containerruntime.DefaultContainerdCertsDir, endpoint, "hosts.toml")
   338  		if err := c.configureContainerdDaemonService(endpoint, src); err != nil {
   339  			return err
   340  		}
   341  	}
   342  
   343  	eg, _ := errgroup.WithContext(context.Background())
   344  
   345  	// for docker: copy daemon.json to "/etc/docker/daemon.json"
   346  	// for containerd : copy hosts.toml to "/etc/containerd/certs.d/${domain}:${port}/hosts.toml"
   347  	for i := range hosts {
   348  		ip := hosts[i]
   349  		eg.Go(func() error {
   350  			err := c.infraDriver.Copy(ip, src, dest)
   351  			if err != nil {
   352  				return err
   353  			}
   354  
   355  			err = c.infraDriver.CmdAsync(ip, nil, "systemctl daemon-reload")
   356  			if err != nil {
   357  				return err
   358  			}
   359  			return nil
   360  		})
   361  	}
   362  	return eg.Wait()
   363  }
   364  
   365  func (c *localConfigurator) configureDockerDaemonService(endpoint, daemonFile string) error {
   366  	var daemonConf DaemonConfig
   367  
   368  	b, err := os.ReadFile(filepath.Clean(daemonFile))
   369  	if err != nil {
   370  		return err
   371  	}
   372  
   373  	b = bytes.TrimSpace(b)
   374  	// if config file is empty, only add registry config.
   375  	if len(b) != 0 {
   376  		if err := json.Unmarshal(b, &daemonConf); err != nil {
   377  			return fmt.Errorf("failed to load %s to DaemonConfig: %v", daemonFile, err)
   378  		}
   379  	}
   380  
   381  	daemonConf.RegistryMirrors = append(daemonConf.RegistryMirrors, "https://"+endpoint)
   382  
   383  	content, err := json.MarshalIndent(daemonConf, "", "  ")
   384  
   385  	if err != nil {
   386  		return fmt.Errorf("failed to marshal daemonFile: %v", err)
   387  	}
   388  
   389  	return osutils.NewCommonWriter(daemonFile).WriteFile(content)
   390  }
   391  
   392  func (c *localConfigurator) configureContainerdDaemonService(endpoint, hostTomlFile string) error {
   393  	var (
   394  		caFile             = c.Domain + ".crt"
   395  		registryCaCertPath = filepath.Join(c.containerRuntimeInfo.CertsDir, endpoint, caFile)
   396  		url                = "https://" + endpoint
   397  	)
   398  
   399  	cfg := Hosts{
   400  		Server: url,
   401  		HostConfigs: map[string]HostFileConfig{
   402  			url: {CACert: registryCaCertPath},
   403  		},
   404  	}
   405  
   406  	bs, err := toml.Marshal(cfg)
   407  	if err != nil {
   408  		return fmt.Errorf("failed to marshal containerd hosts.toml file: %v", err)
   409  	}
   410  
   411  	return osutils.NewCommonWriter(hostTomlFile).WriteFile(bs)
   412  }
   413  
   414  type Hosts struct {
   415  	// Server specifies the default server. When `host` is
   416  	// also specified, those hosts are tried first.
   417  	Server string `toml:"server"`
   418  	// HostConfigs store the per-host configuration
   419  	HostConfigs map[string]HostFileConfig `toml:"host"`
   420  }
   421  
   422  type HostFileConfig struct {
   423  	// CACert are the public key certificates for TLS
   424  	// Accepted types
   425  	// - string - Single file with certificate(s)
   426  	// - []string - Multiple files with certificates
   427  	CACert interface{} `toml:"ca"`
   428  }
   429  
   430  type DaemonConfig struct {
   431  	AllowNonDistributableArtifacts []string          `json:"allow-nondistributable-artifacts,omitempty"`
   432  	APICorsHeader                  string            `json:"api-cors-header,omitempty"`
   433  	AuthorizationPlugins           []string          `json:"authorization-plugins,omitempty"`
   434  	Bip                            string            `json:"bip,omitempty"`
   435  	Bridge                         string            `json:"bridge,omitempty"`
   436  	CgroupParent                   string            `json:"cgroup-parent,omitempty"`
   437  	ClusterAdvertise               string            `json:"cluster-advertise,omitempty"`
   438  	ClusterStore                   string            `json:"cluster-store,omitempty"`
   439  	Containerd                     string            `json:"containerd,omitempty"`
   440  	ContainerdNamespace            string            `json:"containerd-namespace,omitempty"`
   441  	ContainerdPluginNamespace      string            `json:"containerd-plugin-namespace,omitempty"`
   442  	DataRoot                       string            `json:"data-root,omitempty"`
   443  	Debug                          bool              `json:"debug,omitempty"`
   444  	DefaultCgroupnsMode            string            `json:"default-cgroupns-mode,omitempty"`
   445  	DefaultGateway                 string            `json:"default-gateway,omitempty"`
   446  	DefaultGatewayV6               string            `json:"default-gateway-v6,omitempty"`
   447  	DefaultRuntime                 string            `json:"default-runtime,omitempty"`
   448  	DefaultShmSize                 string            `json:"default-shm-size,omitempty"`
   449  	DNS                            []string          `json:"dns,omitempty"`
   450  	DNSOpts                        []string          `json:"dns-opts,omitempty"`
   451  	DNSSearch                      []string          `json:"dns-search,omitempty"`
   452  	ExecOpts                       []string          `json:"exec-opts,omitempty"`
   453  	ExecRoot                       string            `json:"exec-root,omitempty"`
   454  	Experimental                   bool              `json:"experimental,omitempty"`
   455  	FixedCidr                      string            `json:"fixed-cidr,omitempty"`
   456  	FixedCidrV6                    string            `json:"fixed-cidr-v6,omitempty"`
   457  	Group                          string            `json:"group,omitempty"`
   458  	Hosts                          []string          `json:"hosts,omitempty"`
   459  	Icc                            bool              `json:"icc,omitempty"`
   460  	Init                           bool              `json:"init,omitempty"`
   461  	InitPath                       string            `json:"init-path,omitempty"`
   462  	InsecureRegistries             []string          `json:"insecure-registries,omitempty"`
   463  	IP                             string            `json:"ip,omitempty"`
   464  	IPForward                      bool              `json:"ip-forward,omitempty"`
   465  	IPMasq                         bool              `json:"ip-masq,omitempty"`
   466  	Iptables                       bool              `json:"iptables,omitempty"`
   467  	IP6Tables                      bool              `json:"ip6tables,omitempty"`
   468  	Ipv6                           bool              `json:"ipv6,omitempty"`
   469  	Labels                         []string          `json:"labels,omitempty"`
   470  	LiveRestore                    bool              `json:"live-restore,omitempty"`
   471  	LogDriver                      string            `json:"log-driver,omitempty"`
   472  	LogLevel                       string            `json:"log-level,omitempty"`
   473  	MaxConcurrentDownloads         int               `json:"max-concurrent-downloads,omitempty"`
   474  	MaxConcurrentUploads           int               `json:"max-concurrent-uploads,omitempty"`
   475  	MaxDownloadAttempts            int               `json:"max-download-attempts,omitempty"`
   476  	Mtu                            int               `json:"mtu,omitempty"`
   477  	NoNewPrivileges                bool              `json:"no-new-privileges,omitempty"`
   478  	NodeGenericResources           []string          `json:"node-generic-resources,omitempty"`
   479  	OomScoreAdjust                 int               `json:"oom-score-adjust,omitempty"`
   480  	Pidfile                        string            `json:"pidfile,omitempty"`
   481  	RawLogs                        bool              `json:"raw-logs,omitempty"`
   482  	RegistryMirrors                []string          `json:"registry-mirrors,omitempty"`
   483  	SeccompProfile                 string            `json:"seccomp-profile,omitempty"`
   484  	SelinuxEnabled                 bool              `json:"selinux-enabled,omitempty"`
   485  	ShutdownTimeout                int               `json:"shutdown-timeout,omitempty"`
   486  	StorageDriver                  string            `json:"storage-driver,omitempty"`
   487  	StorageOpts                    []string          `json:"storage-opts,omitempty"`
   488  	SwarmDefaultAdvertiseAddr      string            `json:"swarm-default-advertise-addr,omitempty"`
   489  	TLS                            bool              `json:"tls,omitempty"`
   490  	Tlscacert                      string            `json:"tlscacert,omitempty"`
   491  	Tlscert                        string            `json:"tlscert,omitempty"`
   492  	Tlskey                         string            `json:"tlskey,omitempty"`
   493  	Tlsverify                      bool              `json:"tlsverify,omitempty"`
   494  	UserlandProxy                  bool              `json:"userland-proxy,omitempty"`
   495  	UserlandProxyPath              string            `json:"userland-proxy-path,omitempty"`
   496  	UsernsRemap                    string            `json:"userns-remap,omitempty"`
   497  	ClusterStoreOpts               map[string]string `json:"cluster-store-opts,omitempty"`
   498  	LogOpts                        *DaemonLogOpts    `json:"log-opts,omitempty"`
   499  }
   500  
   501  type DaemonLogOpts struct {
   502  	CacheDisabled string `json:"cache-disabled,omitempty"`
   503  	CacheMaxFile  string `json:"cache-max-file,omitempty"`
   504  	CacheMaxSize  string `json:"cache-max-size,omitempty"`
   505  	CacheCompress string `json:"cache-compress,omitempty"`
   506  	Env           string `json:"env,omitempty"`
   507  	Labels        string `json:"labels,omitempty"`
   508  	MaxFile       string `json:"max-file,omitempty"`
   509  	MaxSize       string `json:"max-size,omitempty"`
   510  }
   511  
   512  func GetRegistryVIP(infraDriver infradriver.InfraDriver) string {
   513  	vip := common.DefaultVIP
   514  	if hosts := infraDriver.GetHostIPList(); len(hosts) > 0 && k8snet.IsIPv6(hosts[0]) {
   515  		vip = common.DefaultVIPForIPv6
   516  	}
   517  
   518  	if ipv4, ok := infraDriver.GetClusterEnv()[common.EnvIPvsVIPForIPv4]; ok {
   519  		vip = ipv4
   520  	}
   521  
   522  	if ipv6, ok := infraDriver.GetClusterEnv()[common.EnvIPvsVIPForIPv6]; ok {
   523  		vip = ipv6
   524  	}
   525  	return vip
   526  }