github.com/dctrud/umoci@v0.4.3-0.20191016193643-05a1d37de015/oci/config/convert/runtime.go (about)

     1  /*
     2   * umoci: Umoci Modifies Open Containers' Images
     3   * Copyright (C) 2016, 2017, 2018 SUSE LLC.
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *    http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17  
    18  package convert
    19  
    20  import (
    21  	"path/filepath"
    22  	"runtime"
    23  	"strings"
    24  
    25  	"github.com/apex/log"
    26  	igen "github.com/openSUSE/umoci/oci/config/generate"
    27  	"github.com/openSUSE/umoci/third_party/user"
    28  	ispec "github.com/opencontainers/image-spec/specs-go/v1"
    29  	rspec "github.com/opencontainers/runtime-spec/specs-go"
    30  	rgen "github.com/opencontainers/runtime-tools/generate"
    31  	"github.com/pkg/errors"
    32  )
    33  
    34  // Annotations described by the OCI image-spec document (these represent fields
    35  // in an image configuration that do not have a native representation in the
    36  // runtime-spec).
    37  const (
    38  	authorAnnotation       = "org.opencontainers.image.author"
    39  	createdAnnotation      = "org.opencontainers.image.created"
    40  	stopSignalAnnotation   = "org.opencontainers.image.stopSignal"
    41  	exposedPortsAnnotation = "org.opencontainers.image.exposedPorts"
    42  )
    43  
    44  // ToRuntimeSpec converts the given OCI image configuration to a runtime
    45  // configuration appropriate for use, which is templated on the default
    46  // configuration specified by the OCI runtime-tools. It is equivalent to
    47  // MutateRuntimeSpec("runtime-tools/generate".New(), image).Spec().
    48  func ToRuntimeSpec(rootfs string, image ispec.Image) (rspec.Spec, error) {
    49  	g, err := rgen.New(runtime.GOOS)
    50  	if err != nil {
    51  		return rspec.Spec{}, err
    52  	}
    53  	if err := MutateRuntimeSpec(g, rootfs, image); err != nil {
    54  		return rspec.Spec{}, err
    55  	}
    56  	return *g.Spec(), nil
    57  }
    58  
    59  // parseEnv splits a given environment variable (of the form name=value) into
    60  // (name, value). An error is returned if there is no "=" in the line or if the
    61  // name is empty.
    62  func parseEnv(env string) (string, string, error) {
    63  	parts := strings.SplitN(env, "=", 2)
    64  	if len(parts) != 2 {
    65  		return "", "", errors.Errorf("environment variable must contain '=': %s", env)
    66  	}
    67  
    68  	name, value := parts[0], parts[1]
    69  	if name == "" {
    70  		return "", "", errors.Errorf("environment variable must have non-empty name: %s", env)
    71  	}
    72  	return name, value, nil
    73  }
    74  
    75  // MutateRuntimeSpec mutates a given runtime specification generator with the
    76  // image configuration provided. It returns the original generator, and does
    77  // not modify any fields directly (to allow for chaining).
    78  func MutateRuntimeSpec(g rgen.Generator, rootfs string, image ispec.Image) error {
    79  	ig, err := igen.NewFromImage(image)
    80  	if err != nil {
    81  		return errors.Wrap(err, "creating image generator")
    82  	}
    83  
    84  	if ig.OS() != "linux" {
    85  		return errors.Errorf("unsupported OS: %s", image.OS)
    86  	}
    87  
    88  	// FIXME: We need to figure out if we're modifying an incompatible runtime spec.
    89  	//g.SetVersion(rspec.Version)
    90  	// TODO: We stopped including the OS and Architecture information in the runtime-spec.
    91  	//       Make sure we fix that once https://github.com/opencontainers/image-spec/pull/711
    92  	//       is resolved.
    93  
    94  	// Set verbatim fields
    95  	g.SetProcessTerminal(true)
    96  	g.SetRootPath(filepath.Base(rootfs))
    97  	g.SetRootReadonly(false)
    98  
    99  	g.SetProcessCwd("/")
   100  	if ig.ConfigWorkingDir() != "" {
   101  		g.SetProcessCwd(ig.ConfigWorkingDir())
   102  	}
   103  
   104  	for _, env := range ig.ConfigEnv() {
   105  		name, value, err := parseEnv(env)
   106  		if err != nil {
   107  			return errors.Wrap(err, "parsing image.Config.Env")
   108  		}
   109  		g.AddProcessEnv(name, value)
   110  	}
   111  
   112  	args := []string{}
   113  	args = append(args, ig.ConfigEntrypoint()...)
   114  	args = append(args, ig.ConfigCmd()...)
   115  	if len(args) > 0 {
   116  		g.SetProcessArgs(args)
   117  	}
   118  
   119  	// Set annotations fields
   120  	for key, value := range ig.ConfigLabels() {
   121  		g.AddAnnotation(key, value)
   122  	}
   123  	g.AddAnnotation(authorAnnotation, ig.Author())
   124  	g.AddAnnotation(createdAnnotation, ig.Created().Format(igen.ISO8601))
   125  	g.AddAnnotation(stopSignalAnnotation, image.Config.StopSignal)
   126  
   127  	// Set parsed fields
   128  	// Get the *actual* uid and gid of the user. If the image doesn't contain
   129  	// an /etc/passwd or /etc/group file then GetExecUserPath will just do a
   130  	// numerical parsing.
   131  	var passwdPath, groupPath string
   132  	if rootfs != "" {
   133  		passwdPath = filepath.Join(rootfs, "/etc/passwd")
   134  		groupPath = filepath.Join(rootfs, "/etc/group")
   135  	}
   136  	execUser, err := user.GetExecUserPath(ig.ConfigUser(), nil, passwdPath, groupPath)
   137  	if err != nil {
   138  		// We only log an error if were not given a rootfs, and we set execUser
   139  		// to the "default" (root:root).
   140  		if rootfs != "" {
   141  			return errors.Wrapf(err, "cannot parse user spec: '%s'", ig.ConfigUser())
   142  		}
   143  		log.Warnf("could not parse user spec '%s' without a rootfs -- defaulting to root:root", ig.ConfigUser())
   144  		execUser = new(user.ExecUser)
   145  	}
   146  
   147  	g.SetProcessUID(uint32(execUser.Uid))
   148  	g.SetProcessGID(uint32(execUser.Gid))
   149  	g.ClearProcessAdditionalGids()
   150  
   151  	for _, gid := range execUser.Sgids {
   152  		g.AddProcessAdditionalGid(uint32(gid))
   153  	}
   154  	if execUser.Home != "" {
   155  		g.AddProcessEnv("HOME", execUser.Home)
   156  	}
   157  
   158  	// Set optional fields
   159  	ports := ig.ConfigExposedPortsArray()
   160  	g.AddAnnotation(exposedPortsAnnotation, strings.Join(ports, ","))
   161  
   162  	for vol := range ig.ConfigVolumes() {
   163  		// XXX: This is _fine_ but might cause some issues in the future.
   164  		g.AddMount(rspec.Mount{
   165  			Destination: vol,
   166  			Type:        "tmpfs",
   167  			Source:      "none",
   168  			Options:     []string{"rw", "nosuid", "nodev", "noexec", "relatime"},
   169  		})
   170  	}
   171  
   172  	// Remove all seccomp rules.
   173  	g.Spec().Linux.Seccomp = nil
   174  
   175  	return nil
   176  }