github.com/jlmeeker/kismatic@v1.10.1-0.20180612190640-57f9005a1f1a/pkg/cli/seed.go (about)

     1  package cli
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  	"strings"
    12  
    13  	yaml "gopkg.in/yaml.v2"
    14  
    15  	"github.com/apprenda/kismatic/pkg/install"
    16  	"github.com/apprenda/kismatic/pkg/util"
    17  	"github.com/spf13/cobra"
    18  )
    19  
    20  const seedRegistryLong = `
    21  Seed a registry with the container images required by KET during the installation
    22  or upgrade of your Kubernetes cluster.
    23  
    24  The docker command line client must be installed on this machine to be able 
    25  to push the images to the registry.
    26  
    27  The location of the registry is obtained from the plan file by default. If you
    28  don't have a plan file, you can pass the location of the registry using the 
    29  --server flag. The server specified through the flag takes precedence over the 
    30  one defined in the plan file.
    31  
    32  If you want to further control how your registry is seeded, or if you are only
    33  interested in the list of all images that can be used in a KET installation, you
    34  may use the --list-only flag.
    35  `
    36  
    37  const imagesManifestFile = "./ansible/playbooks/group_vars/container_images.yaml"
    38  
    39  type seedRegistryOptions struct {
    40  	listOnly            bool
    41  	verbose             bool
    42  	planFile            string
    43  	imagesManifestsFile string
    44  	registryServer      string
    45  }
    46  
    47  type imageManifest struct {
    48  	OfficialImages map[string]image `yaml:"official_images"`
    49  }
    50  
    51  type image struct {
    52  	Name    string `yaml:"name"`
    53  	Version string `yaml:"version"`
    54  }
    55  
    56  func (i image) String() string {
    57  	return fmt.Sprintf("%s:%s", i.Name, i.Version)
    58  }
    59  
    60  // NewCmdSeedRegistry returns the command for seeding a container image registry
    61  // with the images required by KET
    62  func NewCmdSeedRegistry(stdout, stderr io.Writer) *cobra.Command {
    63  	var options seedRegistryOptions
    64  	cmd := &cobra.Command{
    65  		Use:   "seed-registry",
    66  		Short: "seed a registry with the container images required by KET",
    67  		Long:  seedRegistryLong,
    68  		RunE: func(cmd *cobra.Command, args []string) error {
    69  			if len(args) != 0 {
    70  				return cmd.Usage()
    71  			}
    72  			if options.listOnly {
    73  				return doListImages(stdout, options)
    74  			}
    75  			return doSeedRegistry(stdout, stderr, options)
    76  		},
    77  	}
    78  	cmd.Flags().BoolVar(&options.listOnly, "list-only", false, "when true, the images will only be listed but not pushed to the registry")
    79  	cmd.Flags().BoolVar(&options.verbose, "verbose", false, "enable verbose logging")
    80  	cmd.Flags().StringVar(&options.registryServer, "server", "", "set to the location of the registry server, without the protocol (e.g. localhost:5000)")
    81  	cmd.Flags().StringVar(&options.imagesManifestsFile, "images-manifest-file", "", "path to the container images manifest file")
    82  	addPlanFileFlag(cmd.Flags(), &options.planFile)
    83  	return cmd
    84  }
    85  
    86  func doListImages(stdout io.Writer, options seedRegistryOptions) error {
    87  	// try to get path relative to executable
    88  	manifest := manifestPath(options.imagesManifestsFile)
    89  	// set default image versions
    90  	versions := install.VersionOverrides()
    91  
    92  	// try to read the plan file to get component versions
    93  	planner := install.FilePlanner{File: options.planFile}
    94  	if planner.PlanExists() {
    95  		plan, err := planner.Read()
    96  		if err != nil {
    97  			util.PrettyPrintErr(stdout, "Reading installation plan file %q", options.planFile)
    98  			return fmt.Errorf("error reading plan file: %v", err)
    99  		}
   100  		// set versions from the plan file
   101  		versions = plan.Versions()
   102  	}
   103  
   104  	im, err := readImageManifest(manifest, versions)
   105  	if err != nil {
   106  		return err
   107  	}
   108  	for _, img := range im.OfficialImages {
   109  		fmt.Fprintf(stdout, "%s\n", img)
   110  	}
   111  	return nil
   112  }
   113  
   114  func doSeedRegistry(stdout, stderr io.Writer, options seedRegistryOptions) error {
   115  	util.PrintHeader(stdout, "Seed Container Image Registry", '=')
   116  
   117  	// Validate that docker is available
   118  	if _, err := exec.LookPath("docker"); err != nil {
   119  		return errors.New("Did not find docker installed on this node. The docker CLI must be available for seeding the registry.")
   120  	}
   121  
   122  	// try to get path relative to executable
   123  	manifest := manifestPath(options.imagesManifestsFile)
   124  	// set default image versions
   125  	versions := install.VersionOverrides()
   126  
   127  	// Figure out the registry we are to seed
   128  	// The registry specified through the command-line flag takes precedence
   129  	// over the one defined in the plan file.
   130  	server := options.registryServer
   131  	if server == "" {
   132  		// we need to get the server from the plan file
   133  		planner := install.FilePlanner{File: options.planFile}
   134  		if !planner.PlanExists() {
   135  			util.PrettyPrintErr(stdout, "Reading installation plan file %q", options.planFile)
   136  			fmt.Fprintln(stdout, `Run "kismatic install plan" to generate it or use the "--server" option`)
   137  			return fmt.Errorf("plan does not exist")
   138  		}
   139  		plan, err := planner.Read()
   140  		if err != nil {
   141  			util.PrettyPrintErr(stdout, "Reading installation plan file %q", options.planFile)
   142  			return fmt.Errorf("error reading plan file: %v", err)
   143  		}
   144  		util.PrettyPrintOk(stdout, "Reading installation plan file %q", options.planFile)
   145  		// Validate the registry info in the plan file
   146  		errs := []error{}
   147  		if plan.DockerRegistry.Server == "" {
   148  			errs = append(errs, errors.New("The private registry's address must be set in the plan file."))
   149  		}
   150  		if len(errs) > 0 {
   151  			util.PrettyPrintErr(stdout, "Validating registry configured in plan file")
   152  			util.PrintValidationErrors(stdout, errs)
   153  			return errors.New("Invalid registry configuration found in plan file")
   154  		}
   155  		server = plan.DockerRegistry.Server
   156  		// set versions from the plan file
   157  		versions = plan.Versions()
   158  	}
   159  
   160  	im, err := readImageManifest(manifest, versions)
   161  	if err != nil {
   162  		return err
   163  	}
   164  
   165  	// Seed the registry with the images
   166  	n := len(im.OfficialImages)
   167  	i := 1
   168  	for _, img := range im.OfficialImages {
   169  		l := fmt.Sprintf("(%d/%d) Seeding %s ", i, n, img)
   170  		pad := 80 - len(l)
   171  		if pad < 0 {
   172  			pad = 0
   173  		}
   174  		fmt.Fprintf(stdout, l+strings.Repeat(" ", pad))
   175  		if err := seedImage(stdout, stderr, img, server, options.verbose); err != nil {
   176  			return fmt.Errorf("Error seeding image %q: %v", img, err)
   177  		}
   178  		util.PrintOkln(stdout)
   179  		i++
   180  	}
   181  
   182  	util.PrintColor(stdout, util.Green, "\nThe registry %q was seeded successfully.\n", server)
   183  	fmt.Fprintln(stdout)
   184  	return nil
   185  }
   186  
   187  func seedImage(stdout, stderr io.Writer, img image, registry string, verbose bool) error {
   188  	runDockerCmd := func(args ...string) error {
   189  		command := exec.Command("docker", args...)
   190  		command.Stderr = stderr
   191  		if verbose {
   192  			command.Stdout = stdout
   193  		}
   194  		return command.Run()
   195  	}
   196  
   197  	// pull
   198  	if err := runDockerCmd("pull", img.String()); err != nil {
   199  		return err
   200  	}
   201  	// tag
   202  	privateImgTag := fmt.Sprintf("%s/%s", registry, img)
   203  	if err := runDockerCmd("tag", img.String(), privateImgTag); err != nil {
   204  		return err
   205  	}
   206  	// push
   207  	if err := runDockerCmd("push", privateImgTag); err != nil {
   208  		return err
   209  	}
   210  	return nil
   211  }
   212  
   213  func readImageManifest(file string, versions map[string]string) (imageManifest, error) {
   214  	im := imageManifest{}
   215  	imBytes, err := ioutil.ReadFile(file)
   216  	if err != nil {
   217  		return im, fmt.Errorf("Error reading the list of images: %v", err)
   218  	}
   219  	if err := yaml.Unmarshal(imBytes, &im); err != nil {
   220  		return im, fmt.Errorf("Error unmarshalling the list of images: %v", err)
   221  	}
   222  	// subsitute versions
   223  	for k, img := range im.OfficialImages {
   224  		if val, ok := versions[k]; ok {
   225  			img.Version = val
   226  			im.OfficialImages[k] = img
   227  		}
   228  	}
   229  	return im, nil
   230  }
   231  
   232  func manifestPath(customManifestPath string) string {
   233  	if customManifestPath != "" {
   234  		return customManifestPath
   235  	}
   236  	manifest := imagesManifestFile
   237  	// to support running the command from not the current path
   238  	// try to get the path of the executable
   239  	ex, err := os.Executable()
   240  	if err == nil {
   241  		exPath := filepath.Dir(ex)
   242  		manifest = filepath.Join(exPath, imagesManifestFile)
   243  	}
   244  
   245  	return manifest
   246  }