github.com/opencontainers/umoci@v0.4.8-0.20240508124516-656e4836fb0d/oci/config/convert/runtime.go (about)

     1  /*
     2   * umoci: Umoci Modifies Open Containers' Images
     3   * Copyright (C) 2016-2020 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  	"strings"
    23  
    24  	"github.com/apex/log"
    25  	"github.com/blang/semver/v4"
    26  	"github.com/moby/sys/user"
    27  	ispec "github.com/opencontainers/image-spec/specs-go/v1"
    28  	rspec "github.com/opencontainers/runtime-spec/specs-go"
    29  	igen "github.com/opencontainers/umoci/oci/config/generate"
    30  	"github.com/pkg/errors"
    31  )
    32  
    33  // Annotations described by the OCI image-spec document (these represent fields
    34  // in an image configuration that do not have a native representation in the
    35  // runtime-spec).
    36  const (
    37  	osAnnotation           = "org.opencontainers.image.os"
    38  	archAnnotation         = "org.opencontainers.image.architecture"
    39  	authorAnnotation       = "org.opencontainers.image.author"
    40  	createdAnnotation      = "org.opencontainers.image.created"
    41  	stopSignalAnnotation   = "org.opencontainers.image.stopSignal"
    42  	exposedPortsAnnotation = "org.opencontainers.image.exposedPorts"
    43  )
    44  
    45  // ToRuntimeSpec converts the given OCI image configuration to a runtime
    46  // configuration appropriate for use, which is templated on the default
    47  // configuration specified by the OCI runtime-tools. It is equivalent to
    48  // MutateRuntimeSpec("runtime-tools/generate".New(), image).Spec().
    49  func ToRuntimeSpec(rootfs string, image ispec.Image) (rspec.Spec, error) {
    50  	spec := Example()
    51  	if err := MutateRuntimeSpec(&spec, rootfs, image); err != nil {
    52  		return rspec.Spec{}, err
    53  	}
    54  	return spec, nil
    55  }
    56  
    57  // parseEnv splits a given environment variable (of the form name=value) into
    58  // (name, value). An error is returned if there is no "=" in the line or if the
    59  // name is empty.
    60  func parseEnv(env string) (string, string, error) {
    61  	parts := strings.SplitN(env, "=", 2)
    62  	if len(parts) != 2 {
    63  		return "", "", errors.Errorf("environment variable must contain '=': %s", env)
    64  	}
    65  
    66  	name, value := parts[0], parts[1]
    67  	if name == "" {
    68  		return "", "", errors.Errorf("environment variable must have non-empty name: %s", env)
    69  	}
    70  	return name, value, nil
    71  }
    72  
    73  // appendEnv takes a (name, value) pair and inserts it into the given
    74  // environment list (overwriting an existing environment if already set).
    75  func appendEnv(env *[]string, name, value string) {
    76  	val := name + "=" + value
    77  	for idx, oldVal := range *env {
    78  		if strings.HasPrefix(oldVal, name+"=") {
    79  			(*env)[idx] = val
    80  			return
    81  		}
    82  	}
    83  	*env = append(*env, val)
    84  }
    85  
    86  // allocateNilStruct recursively enumerates all pointers in the given type and
    87  // replaces them with the zero-value of their associated type. It's a shame
    88  // that this is necessary.
    89  //
    90  // TODO: Switch to doing this recursively with reflect.
    91  func allocateNilStruct(spec *rspec.Spec) {
    92  	if spec.Process == nil {
    93  		spec.Process = &rspec.Process{}
    94  	}
    95  	if spec.Root == nil {
    96  		spec.Root = &rspec.Root{}
    97  	}
    98  	if spec.Linux == nil {
    99  		spec.Linux = &rspec.Linux{}
   100  	}
   101  	if spec.Annotations == nil {
   102  		spec.Annotations = map[string]string{}
   103  	}
   104  }
   105  
   106  // MutateRuntimeSpec mutates a given runtime configuration with the image
   107  // configuration provided.
   108  func MutateRuntimeSpec(spec *rspec.Spec, rootfs string, image ispec.Image) error {
   109  	ig, err := igen.NewFromImage(image)
   110  	if err != nil {
   111  		return errors.Wrap(err, "creating image generator")
   112  	}
   113  
   114  	if ig.OS() != "linux" {
   115  		return errors.Errorf("unsupported OS: %s", image.OS)
   116  	}
   117  
   118  	allocateNilStruct(spec)
   119  
   120  	// Default config to our rspec version if none was specified.
   121  	if spec.Version == "" {
   122  		spec.Version = curSpecVersion.String()
   123  	}
   124  
   125  	// Make sure that the previous version of the spec is compatible with us.
   126  	// We cannot operate on specifications that are newer than us (because we
   127  	// might drop fields that the user finds important).
   128  	oldVersion, err := semver.Parse(spec.Version)
   129  	if err != nil {
   130  		return errors.Wrap(err, "parsing original runtime-spec config version")
   131  	}
   132  	if oldVersion.GT(curSpecVersion) {
   133  		return errors.Errorf("original runtime-spec config version %s is unsupported: %s > %s", oldVersion, oldVersion, curSpecVersion)
   134  	}
   135  	if oldVersion.Major != curSpecVersion.Major {
   136  		return errors.Errorf("original runtime-spec config version %s is incompatible with version %s: mismatching major number", oldVersion, curSpecVersion)
   137  	}
   138  
   139  	// Set verbatim fields
   140  	spec.Process.Terminal = true
   141  	spec.Root.Path = filepath.Base(rootfs)
   142  	spec.Root.Readonly = false
   143  
   144  	spec.Process.Cwd = "/"
   145  	if ig.ConfigWorkingDir() != "" {
   146  		spec.Process.Cwd = ig.ConfigWorkingDir()
   147  	}
   148  
   149  	for _, env := range ig.ConfigEnv() {
   150  		name, value, err := parseEnv(env)
   151  		if err != nil {
   152  			return errors.Wrap(err, "parsing image.Config.Env")
   153  		}
   154  		appendEnv(&spec.Process.Env, name, value)
   155  	}
   156  
   157  	args := []string{}
   158  	args = append(args, ig.ConfigEntrypoint()...)
   159  	args = append(args, ig.ConfigCmd()...)
   160  	if len(args) > 0 {
   161  		spec.Process.Args = args
   162  	}
   163  
   164  	// Set annotations fields
   165  	for key, value := range ig.ConfigLabels() {
   166  		spec.Annotations[key] = value
   167  	}
   168  	spec.Annotations[osAnnotation] = ig.OS()
   169  	spec.Annotations[archAnnotation] = ig.Architecture()
   170  	spec.Annotations[authorAnnotation] = ig.Author()
   171  	spec.Annotations[createdAnnotation] = ig.Created().Format(igen.ISO8601)
   172  	spec.Annotations[stopSignalAnnotation] = image.Config.StopSignal
   173  
   174  	// Set parsed fields
   175  	// Get the *actual* uid and gid of the user. If the image doesn't contain
   176  	// an /etc/passwd or /etc/group file then GetExecUserPath will just do a
   177  	// numerical parsing.
   178  	var passwdPath, groupPath string
   179  	if rootfs != "" {
   180  		passwdPath = filepath.Join(rootfs, "/etc/passwd")
   181  		groupPath = filepath.Join(rootfs, "/etc/group")
   182  	}
   183  	execUser, err := user.GetExecUserPath(ig.ConfigUser(), nil, passwdPath, groupPath)
   184  	if err != nil {
   185  		// We only log an error if were not given a rootfs, and we set execUser
   186  		// to the "default" (root:root).
   187  		if rootfs != "" {
   188  			return errors.Wrapf(err, "cannot parse user spec: '%s'", ig.ConfigUser())
   189  		}
   190  		log.Warnf("could not parse user spec '%s' without a rootfs -- defaulting to root:root", ig.ConfigUser())
   191  		execUser = new(user.ExecUser)
   192  	}
   193  
   194  	spec.Process.User.UID = uint32(execUser.Uid)
   195  	spec.Process.User.GID = uint32(execUser.Gid)
   196  
   197  	spec.Process.User.AdditionalGids = []uint32{}
   198  	for _, sgid := range execUser.Sgids {
   199  		spec.Process.User.AdditionalGids = append(spec.Process.User.AdditionalGids, uint32(sgid))
   200  	}
   201  
   202  	if execUser.Home != "" {
   203  		appendEnv(&spec.Process.Env, "HOME", execUser.Home)
   204  	}
   205  
   206  	// Set optional fields
   207  	ports := ig.ConfigExposedPortsArray()
   208  	spec.Annotations[exposedPortsAnnotation] = strings.Join(ports, ",")
   209  
   210  	for vol := range ig.ConfigVolumes() {
   211  		// XXX: This is _fine_ but might cause some issues in the future.
   212  		spec.Mounts = append(spec.Mounts, rspec.Mount{
   213  			Destination: vol,
   214  			Type:        "tmpfs",
   215  			Source:      "none",
   216  			Options:     []string{"rw", "nosuid", "nodev", "noexec", "relatime"},
   217  		})
   218  	}
   219  
   220  	// Remove all seccomp rules.
   221  	spec.Linux.Seccomp = nil
   222  	return nil
   223  }