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 }