github.com/opencontainers/umoci@v0.4.8-0.20240508124516-656e4836fb0d/cmd/umoci/config.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 main
    19  
    20  import (
    21  	"context"
    22  	"strings"
    23  	"time"
    24  
    25  	"github.com/apex/log"
    26  	ispec "github.com/opencontainers/image-spec/specs-go/v1"
    27  	"github.com/opencontainers/umoci/mutate"
    28  	"github.com/opencontainers/umoci/oci/cas/dir"
    29  	"github.com/opencontainers/umoci/oci/casext"
    30  	igen "github.com/opencontainers/umoci/oci/config/generate"
    31  	"github.com/pkg/errors"
    32  	"github.com/urfave/cli"
    33  )
    34  
    35  // FIXME: We should also implement a raw mode that just does modifications of
    36  //
    37  //	JSON blobs (allowing this all to be used outside of our build setup).
    38  var configCommand = uxHistory(uxTag(cli.Command{
    39  	Name:  "config",
    40  	Usage: "modifies the image configuration of an OCI image",
    41  	ArgsUsage: `--image <image-path>[:<tag>] [--tag <new-tag>]
    42  
    43  Where "<image-path>" is the path to the OCI image, and "<tag>" is the name of
    44  the tagged image from which the config modifications will be based (if not
    45  specified, it defaults to "latest"). "<new-tag>" is the new reference name to
    46  save the new image as, if this is not specified then umoci will replace the old
    47  image.`,
    48  
    49  	// config modifies a particular image manifest.
    50  	Category: "image",
    51  
    52  	// Verify the metadata.
    53  	Before: func(ctx *cli.Context) error {
    54  		if ctx.NArg() != 0 {
    55  			return errors.Errorf("invalid number of positional arguments: expected none")
    56  		}
    57  		if _, ok := ctx.App.Metadata["--image-path"]; !ok {
    58  			return errors.Errorf("missing mandatory argument: --image")
    59  		}
    60  		if _, ok := ctx.App.Metadata["--image-tag"]; !ok {
    61  			return errors.Errorf("missing mandatory argument: --image")
    62  		}
    63  		return nil
    64  	},
    65  
    66  	// Do not re-order arguments.
    67  	//
    68  	// It turns out that urfave/cli incorrectly handles cases like
    69  	// [--config.cmd -c] during argument re-ordering for subcommands, causing
    70  	// us a fair number of issues when users are trying to pass a flag an
    71  	// argument that starts with a dash. Luckily 'umoci config' doesn't take
    72  	// positional arguments, so disabling argument re-ordering has no other
    73  	// real effect.
    74  	//
    75  	// See <https://github.com/urfave/cli/issues/1152> for more details.
    76  	SkipArgReorder: true,
    77  
    78  	Flags: []cli.Flag{
    79  		cli.StringFlag{Name: "config.user"},
    80  		cli.StringSliceFlag{Name: "config.exposedports"},
    81  		cli.StringSliceFlag{Name: "config.env"},
    82  		cli.StringSliceFlag{Name: "config.entrypoint"}, // FIXME: This interface is weird.
    83  		cli.StringSliceFlag{Name: "config.cmd"},        // FIXME: This interface is weird.
    84  		cli.StringSliceFlag{Name: "config.volume"},
    85  		cli.StringSliceFlag{Name: "config.label"},
    86  		cli.StringFlag{Name: "config.workingdir"},
    87  		cli.StringFlag{Name: "config.stopsignal"},
    88  		cli.StringFlag{Name: "created"}, // FIXME: Implement TimeFlag.
    89  		cli.StringFlag{Name: "author"},
    90  		cli.StringFlag{Name: "architecture"},
    91  		cli.StringFlag{Name: "os"},
    92  		cli.StringSliceFlag{Name: "manifest.annotation"},
    93  		cli.StringSliceFlag{Name: "clear"},
    94  	},
    95  
    96  	Action: config,
    97  }))
    98  
    99  func toImage(config ispec.ImageConfig, meta mutate.Meta) ispec.Image {
   100  	created := meta.Created
   101  	return ispec.Image{
   102  		Config:       config,
   103  		Created:      &created,
   104  		Author:       meta.Author,
   105  		Architecture: meta.Architecture,
   106  		OS:           meta.OS,
   107  	}
   108  }
   109  
   110  func fromImage(image ispec.Image) (ispec.ImageConfig, mutate.Meta) {
   111  	var created time.Time
   112  	if image.Created != nil {
   113  		created = *image.Created
   114  	}
   115  	return image.Config, mutate.Meta{
   116  		Created:      created,
   117  		Author:       image.Author,
   118  		Architecture: image.Architecture,
   119  		OS:           image.OS,
   120  	}
   121  }
   122  
   123  // parseKV splits a given string (of the form name=value) into (name,
   124  // value). An error is returned if there is no "=" in the line or if the
   125  // name is empty.
   126  func parseKV(input string) (string, string, error) {
   127  	parts := strings.SplitN(input, "=", 2)
   128  	if len(parts) != 2 {
   129  		return "", "", errors.Errorf("must contain '=': %s", input)
   130  	}
   131  
   132  	name, value := parts[0], parts[1]
   133  	if name == "" {
   134  		return "", "", errors.Errorf("must have non-empty name: %s", input)
   135  	}
   136  	return name, value, nil
   137  }
   138  
   139  func config(ctx *cli.Context) error {
   140  	imagePath := ctx.App.Metadata["--image-path"].(string)
   141  	fromName := ctx.App.Metadata["--image-tag"].(string)
   142  
   143  	// By default we clobber the old tag.
   144  	tagName := fromName
   145  	if val, ok := ctx.App.Metadata["--tag"]; ok {
   146  		tagName = val.(string)
   147  	}
   148  
   149  	// Get a reference to the CAS.
   150  	engine, err := dir.Open(imagePath)
   151  	if err != nil {
   152  		return errors.Wrap(err, "open CAS")
   153  	}
   154  	engineExt := casext.NewEngine(engine)
   155  	defer engine.Close()
   156  
   157  	fromDescriptorPaths, err := engineExt.ResolveReference(context.Background(), fromName)
   158  	if err != nil {
   159  		return errors.Wrap(err, "get descriptor")
   160  	}
   161  	if len(fromDescriptorPaths) == 0 {
   162  		return errors.Errorf("tag not found: %s", fromName)
   163  	}
   164  	if len(fromDescriptorPaths) != 1 {
   165  		// TODO: Handle this more nicely.
   166  		return errors.Errorf("tag is ambiguous: %s", fromName)
   167  	}
   168  
   169  	mutator, err := mutate.New(engine, fromDescriptorPaths[0])
   170  	if err != nil {
   171  		return errors.Wrap(err, "create mutator for manifest")
   172  	}
   173  
   174  	config, err := mutator.Config(context.Background())
   175  	if err != nil {
   176  		return errors.Wrap(err, "get base config")
   177  	}
   178  
   179  	imageMeta, err := mutator.Meta(context.Background())
   180  	if err != nil {
   181  		return errors.Wrap(err, "get base metadata")
   182  	}
   183  
   184  	annotations, err := mutator.Annotations(context.Background())
   185  	if err != nil {
   186  		return errors.Wrap(err, "get base annotations")
   187  	}
   188  
   189  	g, err := igen.NewFromImage(toImage(config.Config, imageMeta))
   190  	if err != nil {
   191  		return errors.Wrap(err, "create new generator")
   192  	}
   193  
   194  	if ctx.IsSet("clear") {
   195  		for _, key := range ctx.StringSlice("clear") {
   196  			switch key {
   197  			case "config.labels":
   198  				g.ClearConfigLabels()
   199  			case "manifest.annotations":
   200  				annotations = nil
   201  			case "config.exposedports":
   202  				g.ClearConfigExposedPorts()
   203  			case "config.env":
   204  				g.ClearConfigEnv()
   205  			case "config.volume":
   206  				g.ClearConfigVolumes()
   207  			case "rootfs.diffids":
   208  				//g.ClearRootfsDiffIDs()
   209  				return errors.Errorf("--clear=rootfs.diffids is not safe")
   210  			case "config.cmd":
   211  				g.ClearConfigCmd()
   212  			case "config.entrypoint":
   213  				g.ClearConfigEntrypoint()
   214  			default:
   215  				return errors.Errorf("unknown key to --clear: %s", key)
   216  			}
   217  		}
   218  	}
   219  
   220  	if ctx.IsSet("created") {
   221  		// How do we handle other formats?
   222  		created, err := time.Parse(igen.ISO8601, ctx.String("created"))
   223  		if err != nil {
   224  			return errors.Wrap(err, "parse --created")
   225  		}
   226  		g.SetCreated(created)
   227  	}
   228  	if ctx.IsSet("author") {
   229  		g.SetAuthor(ctx.String("author"))
   230  	}
   231  	if ctx.IsSet("architecture") {
   232  		g.SetArchitecture(ctx.String("architecture"))
   233  	}
   234  	if ctx.IsSet("os") {
   235  		g.SetOS(ctx.String("os"))
   236  	}
   237  	if ctx.IsSet("config.user") {
   238  		g.SetConfigUser(ctx.String("config.user"))
   239  	}
   240  	if ctx.IsSet("config.stopsignal") {
   241  		g.SetConfigStopSignal(ctx.String("config.stopsignal"))
   242  	}
   243  	if ctx.IsSet("config.workingdir") {
   244  		g.SetConfigWorkingDir(ctx.String("config.workingdir"))
   245  	}
   246  	if ctx.IsSet("config.exposedports") {
   247  		for _, port := range ctx.StringSlice("config.exposedports") {
   248  			g.AddConfigExposedPort(port)
   249  		}
   250  	}
   251  	if ctx.IsSet("config.env") {
   252  		for _, env := range ctx.StringSlice("config.env") {
   253  			name, value, err := parseKV(env)
   254  			if err != nil {
   255  				return errors.Wrap(err, "config.env")
   256  			}
   257  			g.AddConfigEnv(name, value)
   258  		}
   259  	}
   260  	// FIXME: This interface is weird.
   261  	if ctx.IsSet("config.entrypoint") {
   262  		g.SetConfigEntrypoint(ctx.StringSlice("config.entrypoint"))
   263  	}
   264  	// FIXME: This interface is weird.
   265  	if ctx.IsSet("config.cmd") {
   266  		g.SetConfigCmd(ctx.StringSlice("config.cmd"))
   267  	}
   268  	if ctx.IsSet("config.volume") {
   269  		for _, volume := range ctx.StringSlice("config.volume") {
   270  			g.AddConfigVolume(volume)
   271  		}
   272  	}
   273  	if ctx.IsSet("config.label") {
   274  		for _, label := range ctx.StringSlice("config.label") {
   275  			name, value, err := parseKV(label)
   276  			if err != nil {
   277  				return errors.Wrap(err, "config.label")
   278  			}
   279  			g.AddConfigLabel(name, value)
   280  		}
   281  	}
   282  	if ctx.IsSet("manifest.annotation") {
   283  		if annotations == nil {
   284  			annotations = map[string]string{}
   285  		}
   286  		for _, label := range ctx.StringSlice("manifest.annotation") {
   287  			parts := strings.SplitN(label, "=", 2)
   288  			annotations[parts[0]] = parts[1]
   289  		}
   290  	}
   291  
   292  	var history *ispec.History
   293  	if !ctx.Bool("no-history") {
   294  		created := time.Now()
   295  		history = &ispec.History{
   296  			Author:     g.Author(),
   297  			Comment:    "",
   298  			Created:    &created,
   299  			CreatedBy:  "umoci config",
   300  			EmptyLayer: true,
   301  		}
   302  
   303  		if ctx.IsSet("history.author") {
   304  			history.Author = ctx.String("history.author")
   305  		}
   306  		if ctx.IsSet("history.comment") {
   307  			history.Comment = ctx.String("history.comment")
   308  		}
   309  		if ctx.IsSet("history.created") {
   310  			created, err := time.Parse(igen.ISO8601, ctx.String("history.created"))
   311  			if err != nil {
   312  				return errors.Wrap(err, "parsing --history.created")
   313  			}
   314  			history.Created = &created
   315  		}
   316  		if ctx.IsSet("history.created_by") {
   317  			history.CreatedBy = ctx.String("history.created_by")
   318  		}
   319  	}
   320  
   321  	newConfig, newMeta := fromImage(g.Image())
   322  	if err := mutator.Set(context.Background(), newConfig, newMeta, annotations, history); err != nil {
   323  		return errors.Wrap(err, "set modified configuration")
   324  	}
   325  
   326  	newDescriptorPath, err := mutator.Commit(context.Background())
   327  	if err != nil {
   328  		return errors.Wrap(err, "commit mutated image")
   329  	}
   330  
   331  	log.Infof("new image manifest created: %s->%s", newDescriptorPath.Root().Digest, newDescriptorPath.Descriptor().Digest)
   332  
   333  	if err := engineExt.UpdateReference(context.Background(), tagName, newDescriptorPath.Root()); err != nil {
   334  		return errors.Wrap(err, "add new tag")
   335  	}
   336  
   337  	log.Infof("created new tag for image manifest: %s", tagName)
   338  	return nil
   339  }