diff --git a/cmd/umoci/main.go b/cmd/umoci/main.go index a6c089179..4950c57e1 100644 --- a/cmd/umoci/main.go +++ b/cmd/umoci/main.go @@ -113,6 +113,7 @@ func main() { tagRemoveCommand, tagListCommand, statCommand, + rawSubcommand, } app.Metadata = map[string]interface{}{} @@ -120,7 +121,7 @@ func main() { // In order to make the uxXyz wrappers not too cumbersome we automatically // add them to images with categories set to categoryImage or // categoryLayout. Monkey patching was never this neat. - for idx, cmd := range app.Commands { + for _, cmd := range flattenCommands(app.Commands) { switch cmd.Category { case categoryImage: oldBefore := cmd.Before @@ -136,7 +137,7 @@ func main() { } return nil } - cmd = uxImage(cmd) + *cmd = uxImage(*cmd) case categoryLayout: oldBefore := cmd.Before cmd.Before = func(ctx *cli.Context) error { @@ -148,9 +149,8 @@ func main() { } return nil } - cmd = uxLayout(cmd) + *cmd = uxLayout(*cmd) } - app.Commands[idx] = cmd } // Actually run umoci. diff --git a/cmd/umoci/raw-runtime-config.go b/cmd/umoci/raw-runtime-config.go new file mode 100644 index 000000000..72328bc5a --- /dev/null +++ b/cmd/umoci/raw-runtime-config.go @@ -0,0 +1,171 @@ +/* + * umoci: Umoci Modifies Open Containers' Images + * Copyright (C) 2016, 2017 SUSE LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package main + +import ( + "fmt" + "os" + + "github.com/apex/log" + "github.com/openSUSE/umoci/oci/cas" + "github.com/openSUSE/umoci/oci/casext" + "github.com/openSUSE/umoci/oci/layer" + "github.com/openSUSE/umoci/pkg/idtools" + ispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" + "github.com/urfave/cli" + "golang.org/x/net/context" +) + +var rawConfigCommand = cli.Command{ + Name: "runtime-config", + Aliases: []string{"config"}, + Usage: "generates an OCI runtime configuration for an image", + ArgsUsage: `--image [:] [--rootfs ] + +Where "" is the path to the OCI image, "" is the name of the +tagged image to unpack (if not specified, defaults to "latest"), "" is +a rootfs to use as a supplementary "source of truth" for certain generation +operations and "" is the destination to write the runtime +configuration to. + +Note that the results of this may not agree with umoci-unpack(1) because the +--rootfs flag affects how certain properties are interpreted.`, + + // unpack reads manifest information. + Category: "image", + + Flags: []cli.Flag{ + cli.StringSliceFlag{ + Name: "uid-map", + Usage: "specifies a uid mapping to use when generating config", + }, + cli.StringSliceFlag{ + Name: "gid-map", + Usage: "specifies a gid mapping to use when generating config", + }, + cli.BoolFlag{ + Name: "rootless", + Usage: "generate rootless configuration", + }, + cli.StringFlag{ + Name: "rootfs", + Usage: "path to secondary source of truth (root filesystem)", + }, + }, + + Action: rawConfig, + + Before: func(ctx *cli.Context) error { + if ctx.NArg() != 1 { + return errors.Errorf("invalid number of positional arguments: expected ") + } + if ctx.Args().First() == "" { + return errors.Errorf("config.json path cannot be empty") + } + ctx.App.Metadata["config"] = ctx.Args().First() + return nil + }, +} + +func rawConfig(ctx *cli.Context) error { + imagePath := ctx.App.Metadata["--image-path"].(string) + fromName := ctx.App.Metadata["--image-tag"].(string) + configPath := ctx.App.Metadata["config"].(string) + + var meta UmociMeta + meta.Version = ctx.App.Version + + // Parse map options. + // We need to set mappings if we're in rootless mode. + meta.MapOptions.Rootless = ctx.Bool("rootless") + if meta.MapOptions.Rootless { + if !ctx.IsSet("uid-map") { + ctx.Set("uid-map", fmt.Sprintf("%d:0:1", os.Geteuid())) + } + if !ctx.IsSet("gid-map") { + ctx.Set("gid-map", fmt.Sprintf("%d:0:1", os.Getegid())) + } + } + // Parse and set up the mapping options. + for _, uidmap := range ctx.StringSlice("uid-map") { + idMap, err := idtools.ParseMapping(uidmap) + if err != nil { + return errors.Wrapf(err, "failure parsing --uid-map %s: %s", uidmap) + } + meta.MapOptions.UIDMappings = append(meta.MapOptions.UIDMappings, idMap) + } + for _, gidmap := range ctx.StringSlice("gid-map") { + idMap, err := idtools.ParseMapping(gidmap) + if err != nil { + return errors.Wrapf(err, "failure parsing --gid-map %s: %s", gidmap) + } + meta.MapOptions.GIDMappings = append(meta.MapOptions.GIDMappings, idMap) + } + + log.WithFields(log.Fields{ + "map.uid": meta.MapOptions.UIDMappings, + "map.gid": meta.MapOptions.GIDMappings, + }).Debugf("parsed mappings") + + // Get a reference to the CAS. + engine, err := cas.Open(imagePath) + if err != nil { + return errors.Wrap(err, "open CAS") + } + engineExt := casext.Engine{engine} + defer engine.Close() + + fromDescriptor, err := engineExt.GetReference(context.Background(), fromName) + if err != nil { + return errors.Wrap(err, "get descriptor") + } + meta.From = fromDescriptor + + manifestBlob, err := engineExt.FromDescriptor(context.Background(), meta.From) + if err != nil { + return errors.Wrap(err, "get manifest") + } + defer manifestBlob.Close() + + // FIXME: Implement support for manifest lists. + if manifestBlob.MediaType != ispec.MediaTypeImageManifest { + return errors.Wrap(fmt.Errorf("descriptor does not point to ispec.MediaTypeImageManifest: not implemented: %s", meta.From.MediaType), "invalid --image tag") + } + + // Get the manifest. + manifest, ok := manifestBlob.Data.(ispec.Manifest) + if !ok { + // Should _never_ be reached. + return errors.Errorf("[internal error] unknown manifest blob type: %s", manifestBlob.MediaType) + } + + // Generate the configuration. + configFile, err := os.Create(configPath) + if err != nil { + return errors.Wrap(err, "opening config path") + } + defer configFile.Close() + + // Write out the generated config. + log.Info("generating config.json") + if err := layer.UnpackRuntimeJSON(context.Background(), engineExt, configFile, ctx.String("rootfs"), manifest, &meta.MapOptions); err != nil { + return errors.Wrap(err, "generate config") + } + return nil +} diff --git a/cmd/umoci/raw.go b/cmd/umoci/raw.go new file mode 100644 index 000000000..635d84492 --- /dev/null +++ b/cmd/umoci/raw.go @@ -0,0 +1,38 @@ +/* + * umoci: Umoci Modifies Open Containers' Images + * Copyright (C) 2016, 2017 SUSE LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package main + +import ( + "github.com/urfave/cli" +) + +var rawSubcommand = cli.Command{ + Name: "raw", + Usage: "advanced internal image tooling", + ArgsUsage: `raw [...] + +In order to facilitate more advanced uses of umoci, the umoci-raw(1) +subcommands allow for more fine-grained information to be provided from umoci. +Please do not use these commands if you are not familiar with the intricacies +of the OCI specifications. The top-level umoci-unpack(1) and similar commands +should be sufficient for most use-cases.`, + + Subcommands: []cli.Command{ + rawConfigCommand, + }, +} diff --git a/cmd/umoci/utils_ux.go b/cmd/umoci/utils_ux.go index d731bffb9..f46244737 100644 --- a/cmd/umoci/utils_ux.go +++ b/cmd/umoci/utils_ux.go @@ -29,6 +29,15 @@ import ( // refRegexp defines the regexp that a given OCI tag must obey. var refRegexp = regexp.MustCompile(`^([A-Za-z0-9._-]+)+$`) +func flattenCommands(cmds []cli.Command) []*cli.Command { + var flatten []*cli.Command + for idx, cmd := range cmds { + flatten = append(flatten, &cmds[idx]) + flatten = append(flatten, flattenCommands(cmd.Subcommands)...) + } + return flatten +} + // uxHistory adds the full set of --history.* flags to the given cli.Command as // well as adding relevant validation logic to the .Before of the command. The // values will be stored in ctx.Metadata with the keys "--history.author",