github.com/containerd/nerdctl/v2@v2.0.0-beta.5.0.20240520001846-b5758f54fa28/pkg/dnsutil/hostsstore/updater.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package hostsstore
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"fmt"
    23  	"os"
    24  	"path/filepath"
    25  	"strings"
    26  
    27  	"github.com/containerd/containerd/errdefs"
    28  	"github.com/containerd/log"
    29  	"github.com/containerd/nerdctl/v2/pkg/netutil"
    30  )
    31  
    32  // newUpdater creates an updater for hostsD (/var/lib/nerdctl/<ADDRHASH>/etchosts)
    33  func newUpdater(id, hostsD string) *updater {
    34  	u := &updater{
    35  		id:            id,
    36  		hostsD:        hostsD,
    37  		metaByIPStr:   make(map[string]*Meta),
    38  		nwNameByIPStr: make(map[string]string),
    39  		metaByDir:     make(map[string]*Meta),
    40  	}
    41  	return u
    42  }
    43  
    44  // updater is the struct for updater.update()
    45  type updater struct {
    46  	id            string
    47  	hostsD        string            // "/var/lib/nerdctl/<ADDRHASH>/etchosts"
    48  	metaByIPStr   map[string]*Meta  // key: IP string
    49  	nwNameByIPStr map[string]string // key: IP string, value: key of Meta.Networks
    50  	metaByDir     map[string]*Meta  // key: "/var/lib/nerdctl/<ADDRHASH>/etchosts/<NS>/<ID>"
    51  }
    52  
    53  // update updates the hostsD tree.
    54  // Must be called with a locker for the hostsD directory.
    55  func (u *updater) update() error {
    56  	// phase1: read meta.json
    57  	if err := u.phase1(); err != nil {
    58  		return err
    59  	}
    60  	// phase2: write hosts
    61  	return u.phase2()
    62  }
    63  
    64  // phase1: read meta.json
    65  func (u *updater) phase1() error {
    66  	readMetaWF := func(path string, _ os.FileInfo, walkErr error) error {
    67  		if walkErr != nil {
    68  			return walkErr
    69  		}
    70  		if filepath.Base(path) != metaJSON {
    71  			return nil
    72  		}
    73  		metaB, err := os.ReadFile(path)
    74  		if err != nil {
    75  			return err
    76  		}
    77  		var meta Meta
    78  		if err := json.Unmarshal(metaB, &meta); err != nil {
    79  			return err
    80  		}
    81  		u.metaByDir[filepath.Dir(path)] = &meta
    82  		for nwName, cniRes := range meta.Networks {
    83  			for _, ipCfg := range cniRes.IPs {
    84  				if ip := ipCfg.Address.IP; ip != nil {
    85  					if ip.IsLoopback() || ip.IsUnspecified() {
    86  						continue
    87  					}
    88  					ipStr := ip.String()
    89  					u.metaByIPStr[ipStr] = &meta
    90  					u.nwNameByIPStr[ipStr] = nwName
    91  				}
    92  			}
    93  		}
    94  		return nil
    95  	}
    96  	return filepath.Walk(u.hostsD, readMetaWF)
    97  }
    98  
    99  // phase2: write hosts
   100  func (u *updater) phase2() error {
   101  	writeHostsWF := func(path string, _ os.FileInfo, walkErr error) error {
   102  		if walkErr != nil {
   103  			return walkErr
   104  		}
   105  		if filepath.Base(path) != "hosts" {
   106  			return nil
   107  		}
   108  		dir := filepath.Dir(path)
   109  		myMeta, ok := u.metaByDir[dir]
   110  		if !ok {
   111  			log.L.WithError(errdefs.ErrNotFound).Debugf("hostsstore metadata %q not found in %q?", metaJSON, dir)
   112  			return nil
   113  		}
   114  		myNetworks := make(map[string]struct{})
   115  		for nwName := range myMeta.Networks {
   116  			myNetworks[nwName] = struct{}{}
   117  		}
   118  
   119  		// parse the hosts file, keep the original host record
   120  		// retain custom /etc/hosts entries outside <nerdctl> </nerdctl> region
   121  		r, err := os.Open(path)
   122  		if err != nil {
   123  			return err
   124  		}
   125  		var buf bytes.Buffer
   126  		if r != nil {
   127  			if err := parseHostsButSkipMarkedRegion(&buf, r); err != nil {
   128  				log.L.WithError(err).Warn("failed to read hosts file")
   129  			}
   130  		}
   131  
   132  		buf.WriteString(fmt.Sprintf("# %s\n", MarkerBegin))
   133  		buf.WriteString("127.0.0.1	localhost localhost.localdomain\n")
   134  		buf.WriteString("::1		localhost localhost.localdomain\n")
   135  
   136  		// keep extra hosts first
   137  		for host, ip := range myMeta.ExtraHosts {
   138  			buf.WriteString(fmt.Sprintf("%-15s %s\n", ip, host))
   139  		}
   140  
   141  		for ip, nwName := range u.nwNameByIPStr {
   142  			meta := u.metaByIPStr[ip]
   143  			if line := createLine(nwName, meta, myNetworks); len(line) != 0 {
   144  				buf.WriteString(fmt.Sprintf("%-15s %s\n", ip, strings.Join(line, " ")))
   145  			}
   146  		}
   147  
   148  		buf.WriteString(fmt.Sprintf("# %s\n", MarkerEnd))
   149  		err = os.WriteFile(path, buf.Bytes(), 0644)
   150  		if err != nil {
   151  			return err
   152  		}
   153  		return nil
   154  	}
   155  	return filepath.Walk(u.hostsD, writeHostsWF)
   156  }
   157  
   158  // createLine returns a line string slice.
   159  // line is like "foo foo.nw0 bar bar.nw0\n"
   160  // for `nerdctl --name=foo --hostname=bar --network=n0`.
   161  //
   162  // May return an empty string slice
   163  func createLine(thatNetwork string, meta *Meta, myNetworks map[string]struct{}) []string {
   164  	line := []string{}
   165  	if _, ok := myNetworks[thatNetwork]; !ok {
   166  		// Do not add lines for other networks
   167  		return line
   168  	}
   169  	baseHostnames := []string{meta.Hostname}
   170  	if meta.Name != "" {
   171  		baseHostnames = append(baseHostnames, meta.Name)
   172  	}
   173  
   174  	for _, baseHostname := range baseHostnames {
   175  		line = append(line, baseHostname)
   176  		if thatNetwork != netutil.DefaultNetworkName {
   177  			// Do not add a entry like "foo.bridge"
   178  			line = append(line, baseHostname+"."+thatNetwork)
   179  		}
   180  	}
   181  	return line
   182  }