diff options
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/api/handlers/libpod/volumes.go | 20 | ||||
-rw-r--r-- | pkg/api/handlers/types.go | 12 | ||||
-rw-r--r-- | pkg/api/server/swagger.go | 3 | ||||
-rw-r--r-- | pkg/bindings/test/volumes_test.go | 29 | ||||
-rw-r--r-- | pkg/bindings/volumes/volumes.go | 6 | ||||
-rw-r--r-- | pkg/domain/entities/engine_container.go | 2 | ||||
-rw-r--r-- | pkg/domain/entities/volumes.go | 41 | ||||
-rw-r--r-- | pkg/domain/infra/abi/parse/parse.go | 68 | ||||
-rw-r--r-- | pkg/domain/infra/abi/pods.go | 19 | ||||
-rw-r--r-- | pkg/domain/infra/abi/volumes.go | 38 | ||||
-rw-r--r-- | pkg/domain/infra/tunnel/pods.go | 13 | ||||
-rw-r--r-- | pkg/domain/infra/tunnel/volumes.go | 16 | ||||
-rw-r--r-- | pkg/specgen/pod.go | 140 | ||||
-rw-r--r-- | pkg/specgen/specgen.go | 2 |
14 files changed, 372 insertions, 37 deletions
diff --git a/pkg/api/handlers/libpod/volumes.go b/pkg/api/handlers/libpod/volumes.go index 9b10ee890..06ca1d225 100644 --- a/pkg/api/handlers/libpod/volumes.go +++ b/pkg/api/handlers/libpod/volumes.go @@ -8,8 +8,8 @@ import ( "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" - "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/domain/entities" "github.com/gorilla/schema" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -25,7 +25,7 @@ func CreateVolume(w http.ResponseWriter, r *http.Request) { }{ // override any golang type defaults } - input := handlers.VolumeCreateConfig{} + input := entities.VolumeCreateOptions{} if err := decoder.Decode(&query, r.URL.Query()); err != nil { utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) @@ -46,8 +46,8 @@ func CreateVolume(w http.ResponseWriter, r *http.Request) { if len(input.Label) > 0 { volumeOptions = append(volumeOptions, libpod.WithVolumeLabels(input.Label)) } - if len(input.Opts) > 0 { - parsedOptions, err := shared.ParseVolumeOptions(input.Opts) + if len(input.Options) > 0 { + parsedOptions, err := shared.ParseVolumeOptions(input.Options) if err != nil { utils.InternalServerError(w, err) return @@ -64,7 +64,17 @@ func CreateVolume(w http.ResponseWriter, r *http.Request) { utils.InternalServerError(w, err) return } - utils.WriteResponse(w, http.StatusOK, config) + volResponse := entities.VolumeConfigResponse{ + Name: config.Name, + Labels: config.Labels, + Driver: config.Driver, + MountPoint: config.MountPoint, + CreatedTime: config.CreatedTime, + Options: config.Options, + UID: config.UID, + GID: config.GID, + } + utils.WriteResponse(w, http.StatusOK, volResponse) } func InspectVolume(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go index ce4a9957b..c6b70251b 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -128,18 +128,6 @@ type CreateContainerConfig struct { NetworkingConfig dockerNetwork.NetworkingConfig } -// swagger:model VolumeCreate -type VolumeCreateConfig struct { - // New volume's name. Can be left blank - Name string `schema:"name"` - // Volume driver to use - Driver string `schema:"driver"` - // User-defined key/value metadata. - Label map[string]string `schema:"label"` - // Mapping of driver options and values. - Opts map[string]string `schema:"opts"` -} - // swagger:model IDResponse type IDResponse struct { // ID diff --git a/pkg/api/server/swagger.go b/pkg/api/server/swagger.go index d2cf7503e..2e1a269f2 100644 --- a/pkg/api/server/swagger.go +++ b/pkg/api/server/swagger.go @@ -4,6 +4,7 @@ import ( "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/domain/entities" ) // No such image @@ -155,7 +156,7 @@ type ok struct { type swagVolumeCreateResponse struct { // in:body Body struct { - libpod.VolumeConfig + entities.VolumeConfigResponse } } diff --git a/pkg/bindings/test/volumes_test.go b/pkg/bindings/test/volumes_test.go index 1d5ae1329..9da034d24 100644 --- a/pkg/bindings/test/volumes_test.go +++ b/pkg/bindings/test/volumes_test.go @@ -6,11 +6,10 @@ import ( "net/http" "time" - "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/bindings" "github.com/containers/libpod/pkg/bindings/containers" "github.com/containers/libpod/pkg/bindings/volumes" - - "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/domain/entities" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/onsi/gomega/gexec" @@ -53,13 +52,13 @@ var _ = Describe("Podman volumes", func() { It("create volume", func() { // create a volume with blank config should work - _, err := volumes.Create(connText, handlers.VolumeCreateConfig{}) + _, err := volumes.Create(connText, entities.VolumeCreateOptions{}) Expect(err).To(BeNil()) - vcc := handlers.VolumeCreateConfig{ - Name: "foobar", - Label: nil, - Opts: nil, + vcc := entities.VolumeCreateOptions{ + Name: "foobar", + Label: nil, + Options: nil, } vol, err := volumes.Create(connText, vcc) Expect(err).To(BeNil()) @@ -73,7 +72,7 @@ var _ = Describe("Podman volumes", func() { }) It("inspect volume", func() { - vol, err := volumes.Create(connText, handlers.VolumeCreateConfig{}) + vol, err := volumes.Create(connText, entities.VolumeCreateOptions{}) Expect(err).To(BeNil()) data, err := volumes.Inspect(connText, vol.Name) Expect(err).To(BeNil()) @@ -87,13 +86,13 @@ var _ = Describe("Podman volumes", func() { Expect(code).To(BeNumerically("==", http.StatusNotFound)) // Removing an unused volume should work - vol, err := volumes.Create(connText, handlers.VolumeCreateConfig{}) + vol, err := volumes.Create(connText, entities.VolumeCreateOptions{}) Expect(err).To(BeNil()) err = volumes.Remove(connText, vol.Name, nil) Expect(err).To(BeNil()) // Removing a volume that is being used without force should be 409 - vol, err = volumes.Create(connText, handlers.VolumeCreateConfig{}) + vol, err = volumes.Create(connText, entities.VolumeCreateOptions{}) Expect(err).To(BeNil()) session := bt.runPodman([]string{"run", "-dt", "-v", fmt.Sprintf("%s:/foobar", vol.Name), "--name", "vtest", alpine.name, "top"}) session.Wait(45) @@ -119,7 +118,7 @@ var _ = Describe("Podman volumes", func() { // create a bunch of named volumes and make verify with list volNames := []string{"homer", "bart", "lisa", "maggie", "marge"} for i := 0; i < 5; i++ { - _, err = volumes.Create(connText, handlers.VolumeCreateConfig{Name: volNames[i]}) + _, err = volumes.Create(connText, entities.VolumeCreateOptions{Name: volNames[i]}) Expect(err).To(BeNil()) } vols, err = volumes.List(connText, nil) @@ -152,15 +151,15 @@ var _ = Describe("Podman volumes", func() { Expect(err).To(BeNil()) // Removing an unused volume should work - _, err = volumes.Create(connText, handlers.VolumeCreateConfig{}) + _, err = volumes.Create(connText, entities.VolumeCreateOptions{}) Expect(err).To(BeNil()) vols, err := volumes.Prune(connText) Expect(err).To(BeNil()) Expect(len(vols)).To(BeNumerically("==", 1)) - _, err = volumes.Create(connText, handlers.VolumeCreateConfig{Name: "homer"}) + _, err = volumes.Create(connText, entities.VolumeCreateOptions{Name: "homer"}) Expect(err).To(BeNil()) - _, err = volumes.Create(connText, handlers.VolumeCreateConfig{}) + _, err = volumes.Create(connText, entities.VolumeCreateOptions{}) Expect(err).To(BeNil()) session := bt.runPodman([]string{"run", "-dt", "-v", fmt.Sprintf("%s:/homer", "homer"), "--name", "vtest", alpine.name, "top"}) session.Wait(45) diff --git a/pkg/bindings/volumes/volumes.go b/pkg/bindings/volumes/volumes.go index 0bc818605..a2164e0af 100644 --- a/pkg/bindings/volumes/volumes.go +++ b/pkg/bindings/volumes/volumes.go @@ -8,15 +8,15 @@ import ( "strings" "github.com/containers/libpod/libpod" - "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/domain/entities" jsoniter "github.com/json-iterator/go" ) // Create creates a volume given its configuration. -func Create(ctx context.Context, config handlers.VolumeCreateConfig) (*libpod.VolumeConfig, error) { +func Create(ctx context.Context, config entities.VolumeCreateOptions) (*entities.VolumeConfigResponse, error) { var ( - v libpod.VolumeConfig + v entities.VolumeConfigResponse ) conn, err := bindings.GetClient(ctx) if err != nil { diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index aa2ceb630..5820c12c3 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -10,7 +10,9 @@ type ContainerEngine interface { ContainerExists(ctx context.Context, nameOrId string) (*BoolReport, error) ContainerWait(ctx context.Context, namesOrIds []string, options WaitOptions) ([]WaitReport, error) PodDelete(ctx context.Context, opts PodPruneOptions) (*PodDeleteReport, error) + PodExists(ctx context.Context, nameOrId string) (*BoolReport, error) PodPrune(ctx context.Context) (*PodPruneReport, error) + VolumeCreate(ctx context.Context, opts VolumeCreateOptions) (*IdOrNameResponse, error) VolumeDelete(ctx context.Context, opts VolumeDeleteOptions) (*VolumeDeleteReport, error) VolumePrune(ctx context.Context) (*VolumePruneReport, error) } diff --git a/pkg/domain/entities/volumes.go b/pkg/domain/entities/volumes.go new file mode 100644 index 000000000..ad12d0d01 --- /dev/null +++ b/pkg/domain/entities/volumes.go @@ -0,0 +1,41 @@ +package entities + +import "time" + +// swagger:model VolumeCreate +type VolumeCreateOptions struct { + // New volume's name. Can be left blank + Name string `schema:"name"` + // Volume driver to use + Driver string `schema:"driver"` + // User-defined key/value metadata. + Label map[string]string `schema:"label"` + // Mapping of driver options and values. + Options map[string]string `schema:"opts"` +} + +type IdOrNameResponse struct { + // The Id or Name of an object + IdOrName string +} + +type VolumeConfigResponse struct { + // Name of the volume. + Name string `json:"name"` + Labels map[string]string `json:"labels"` + // The volume driver. Empty string or local does not activate a volume + // driver, all other volumes will. + Driver string `json:"volumeDriver"` + // The location the volume is mounted at. + MountPoint string `json:"mountPoint"` + // Time the volume was created. + CreatedTime time.Time `json:"createdAt,omitempty"` + // Options to pass to the volume driver. For the local driver, this is + // a list of mount options. For other drivers, they are passed to the + // volume driver handling the volume. + Options map[string]string `json:"volumeOptions,omitempty"` + // UID the volume will be created as. + UID int `json:"uid"` + // GID the volume will be created as. + GID int `json:"gid"` +} diff --git a/pkg/domain/infra/abi/parse/parse.go b/pkg/domain/infra/abi/parse/parse.go new file mode 100644 index 000000000..6c0e1ee55 --- /dev/null +++ b/pkg/domain/infra/abi/parse/parse.go @@ -0,0 +1,68 @@ +package parse + +import ( + "strconv" + "strings" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// Handle volume options from CLI. +// Parse "o" option to find UID, GID. +func ParseVolumeOptions(opts map[string]string) ([]libpod.VolumeCreateOption, error) { + libpodOptions := []libpod.VolumeCreateOption{} + volumeOptions := make(map[string]string) + + for key, value := range opts { + switch key { + case "o": + // o has special handling to parse out UID, GID. + // These are separate Libpod options. + splitVal := strings.Split(value, ",") + finalVal := []string{} + for _, o := range splitVal { + // Options will be formatted as either "opt" or + // "opt=value" + splitO := strings.SplitN(o, "=", 2) + switch strings.ToLower(splitO[0]) { + case "uid": + if len(splitO) != 2 { + return nil, errors.Wrapf(define.ErrInvalidArg, "uid option must provide a UID") + } + intUID, err := strconv.Atoi(splitO[1]) + if err != nil { + return nil, errors.Wrapf(err, "cannot convert UID %s to integer", splitO[1]) + } + logrus.Debugf("Removing uid= from options and adding WithVolumeUID for UID %d", intUID) + libpodOptions = append(libpodOptions, libpod.WithVolumeUID(intUID)) + case "gid": + if len(splitO) != 2 { + return nil, errors.Wrapf(define.ErrInvalidArg, "gid option must provide a GID") + } + intGID, err := strconv.Atoi(splitO[1]) + if err != nil { + return nil, errors.Wrapf(err, "cannot convert GID %s to integer", splitO[1]) + } + logrus.Debugf("Removing gid= from options and adding WithVolumeGID for GID %d", intGID) + libpodOptions = append(libpodOptions, libpod.WithVolumeGID(intGID)) + default: + finalVal = append(finalVal, o) + } + } + if len(finalVal) > 0 { + volumeOptions[key] = strings.Join(finalVal, ",") + } + default: + volumeOptions[key] = value + } + } + + if len(volumeOptions) > 0 { + libpodOptions = append(libpodOptions, libpod.WithVolumeOptions(volumeOptions)) + } + + return libpodOptions, nil +} diff --git a/pkg/domain/infra/abi/pods.go b/pkg/domain/infra/abi/pods.go new file mode 100644 index 000000000..de22de68e --- /dev/null +++ b/pkg/domain/infra/abi/pods.go @@ -0,0 +1,19 @@ +// +build ABISupport + +package abi + +import ( + "context" + "github.com/pkg/errors" + + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/domain/entities" +) + +func (ic *ContainerEngine) PodExists(ctx context.Context, nameOrId string) (*entities.BoolReport, error) { + _, err := ic.Libpod.LookupPod(nameOrId) + if err != nil && errors.Cause(err) != define.ErrNoSuchPod { + return nil, err + } + return &entities.BoolReport{Value: err == nil}, nil +} diff --git a/pkg/domain/infra/abi/volumes.go b/pkg/domain/infra/abi/volumes.go new file mode 100644 index 000000000..0783af441 --- /dev/null +++ b/pkg/domain/infra/abi/volumes.go @@ -0,0 +1,38 @@ +// +build ABISupport + +package abi + +import ( + "context" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/domain/infra/abi/parse" +) + +func (ic *ContainerEngine) VolumeCreate(ctx context.Context, opts entities.VolumeCreateOptions) (*entities.IdOrNameResponse, error) { + var ( + volumeOptions []libpod.VolumeCreateOption + ) + if len(opts.Name) > 0 { + volumeOptions = append(volumeOptions, libpod.WithVolumeName(opts.Name)) + } + if len(opts.Driver) > 0 { + volumeOptions = append(volumeOptions, libpod.WithVolumeDriver(opts.Driver)) + } + if len(opts.Label) > 0 { + volumeOptions = append(volumeOptions, libpod.WithVolumeLabels(opts.Label)) + } + if len(opts.Options) > 0 { + parsedOptions, err := parse.ParseVolumeOptions(opts.Options) + if err != nil { + return nil, err + } + volumeOptions = append(volumeOptions, parsedOptions...) + } + vol, err := ic.Libpod.NewVolume(ctx, volumeOptions...) + if err != nil { + return nil, err + } + return &entities.IdOrNameResponse{IdOrName: vol.Name()}, nil +} diff --git a/pkg/domain/infra/tunnel/pods.go b/pkg/domain/infra/tunnel/pods.go new file mode 100644 index 000000000..500069d51 --- /dev/null +++ b/pkg/domain/infra/tunnel/pods.go @@ -0,0 +1,13 @@ +package tunnel + +import ( + "context" + + "github.com/containers/libpod/pkg/bindings/pods" + "github.com/containers/libpod/pkg/domain/entities" +) + +func (ic *ContainerEngine) PodExists(ctx context.Context, nameOrId string) (*entities.BoolReport, error) { + exists, err := pods.Exists(ic.ClientCxt, nameOrId) + return &entities.BoolReport{Value: exists}, err +} diff --git a/pkg/domain/infra/tunnel/volumes.go b/pkg/domain/infra/tunnel/volumes.go new file mode 100644 index 000000000..49cf6a2f6 --- /dev/null +++ b/pkg/domain/infra/tunnel/volumes.go @@ -0,0 +1,16 @@ +package tunnel + +import ( + "context" + + "github.com/containers/libpod/pkg/bindings/volumes" + "github.com/containers/libpod/pkg/domain/entities" +) + +func (ic *ContainerEngine) VolumeCreate(ctx context.Context, opts entities.VolumeCreateOptions) (*entities.IdOrNameResponse, error) { + response, err := volumes.Create(ic.ClientCxt, opts) + if err != nil { + return nil, err + } + return &entities.IdOrNameResponse{IdOrName: response.Name}, nil +} diff --git a/pkg/specgen/pod.go b/pkg/specgen/pod.go new file mode 100644 index 000000000..1aada83c4 --- /dev/null +++ b/pkg/specgen/pod.go @@ -0,0 +1,140 @@ +package specgen + +import ( + "net" + + "github.com/cri-o/ocicni/pkg/ocicni" +) + +// PodBasicConfig contains basic configuration options for pods. +type PodBasicConfig struct { + // Name is the name of the pod. + // If not provided, a name will be generated when the pod is created. + // Optional. + Name string `json:"name,omitempty"` + // Hostname is the pod's hostname. If not set, the name of the pod will + // be used (if a name was not provided here, the name auto-generated for + // the pod will be used). This will be used by the infra container and + // all containers in the pod as long as the UTS namespace is shared. + // Optional. + Hostname string `json:"hostname,omitempty"` + // Labels are key-value pairs that are used to add metadata to pods. + // Optional. + Labels map[string]string `json:"labels,omitempty"` + // NoInfra tells the pod not to create an infra container. If this is + // done, many networking-related options will become unavailable. + // Conflicts with setting any options in PodNetworkConfig, and the + // InfraCommand and InfraImages in this struct. + // Optional. + NoInfra bool `json:"no_infra,omitempty"` + // InfraCommand sets the command that will be used to start the infra + // container. + // If not set, the default set in the Libpod configuration file will be + // used. + // Conflicts with NoInfra=true. + // Optional. + InfraCommand []string `json:"infra_command,omitempty"` + // InfraImage is the image that will be used for the infra container. + // If not set, the default set in the Libpod configuration file will be + // used. + // Conflicts with NoInfra=true. + // Optional. + InfraImage string `json:"infra_image,omitempty"` + // SharedNamespaces instructs the pod to share a set of namespaces. + // Shared namespaces will be joined (by default) by every container + // which joins the pod. + // If not set and NoInfra is false, the pod will set a default set of + // namespaces to share. + // Conflicts with NoInfra=true. + // Optional. + SharedNamespaces []string `json:"shared_namespaces,omitempty"` +} + +// PodNetworkConfig contains networking configuration for a pod. +type PodNetworkConfig struct { + // NetNS is the configuration to use for the infra container's network + // namespace. This network will, by default, be shared with all + // containers in the pod. + // Cannot be set to FromContainer and FromPod. + // Setting this to anything except "" conflicts with NoInfra=true. + // Defaults to Bridge as root and Slirp as rootless. + // Mandatory. + NetNS Namespace `json:"netns,omitempty"` + // StaticIP sets a static IP for the infra container. As the infra + // container's network is used for the entire pod by default, this will + // thus be a static IP for the whole pod. + // Only available if NetNS is set to Bridge (the default for root). + // As such, conflicts with NoInfra=true by proxy. + // Optional. + StaticIP *net.IP `json:"static_ip,omitempty"` + // StaticMAC sets a static MAC for the infra container. As the infra + // container's network is used for the entire pod by default, this will + // thus be a static MAC for the entire pod. + // Only available if NetNS is set to Bridge (the default for root). + // As such, conflicts with NoInfra=true by proxy. + // Optional. + StaticMAC *net.HardwareAddr `json:"static_mac,omitempty"` + // PortMappings is a set of ports to map into the infra container. + // As, by default, containers share their network with the infra + // container, this will forward the ports to the entire pod. + // Only available if NetNS is set to Bridge or Slirp. + // Optional. + PortMappings []ocicni.PortMapping `json:"portmappings,omitempty"` + // CNINetworks is a list of CNI networks that the infra container will + // join. As, by default, containers share their network with the infra + // container, these networks will effectively be joined by the + // entire pod. + // Only available when NetNS is set to Bridge, the default for root. + // Optional. + CNINetworks []string `json:"cni_networks,omitempty"` + // NoManageResolvConf indicates that /etc/resolv.conf should not be + // managed by the pod. Instead, each container will create and manage a + // separate resolv.conf as if they had not joined a pod. + // Conflicts with NoInfra=true and DNSServer, DNSSearch, DNSOption. + // Optional. + NoManageResolvConf bool `json:"no_manage_resolv_conf,omitempty"` + // DNSServer is a set of DNS servers that will be used in the infra + // container's resolv.conf, which will, by default, be shared with all + // containers in the pod. + // If not provided, the host's DNS servers will be used, unless the only + // server set is a localhost address. As the container cannot connect to + // the host's localhost, a default server will instead be set. + // Conflicts with NoInfra=true. + // Optional. + DNSServer []net.IP `json:"dns_server,omitempty"` + // DNSSearch is a set of DNS search domains that will be used in the + // infra container's resolv.conf, which will, by default, be shared with + // all containers in the pod. + // If not provided, DNS search domains from the host's resolv.conf will + // be used. + // Conflicts with NoInfra=true. + // Optional. + DNSSearch []string `json:"dns_search,omitempty"` + // DNSOption is a set of DNS options that will be used in the infra + // container's resolv.conf, which will, by default, be shared with all + // containers in the pod. + // Conflicts with NoInfra=true. + // Optional. + DNSOption []string `json:"dns_option,omitempty"` + // NoManageHosts indicates that /etc/hosts should not be managed by the + // pod. Instead, each container will create a separate /etc/hosts as + // they would if not in a pod. + // Conflicts with HostAdd. + NoManageHosts bool `json:"no_manage_hosts,omitempty"` + // HostAdd is a set of hosts that will be added to the infra container's + // /etc/hosts that will, by default, be shared with all containers in + // the pod. + // Conflicts with NoInfra=true and NoManageHosts. + // Optional. + HostAdd []string `json:"hostadd,omitempty"` +} + +// PodCgroupConfig contains configuration options about a pod's cgroups. +// This will be expanded in future updates to pods. +type PodCgroupConfig struct { + // CgroupParent is the parent for the CGroup that the pod will create. + // This pod cgroup will, in turn, be the default cgroup parent for all + // containers in the pod. + // Optional. + CgroupParent string `json:"cgroup_parent,omitempty"` +} diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index 7a430652a..b123c1da5 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -53,7 +53,7 @@ type ContainerBasicConfig struct { Terminal bool `json:"terminal,omitempty"` // Stdin is whether the container will keep its STDIN open. Stdin bool `json:"stdin,omitempty"` - // Labels are key-valid labels that are used to add metadata to + // Labels are key-value pairs that are used to add metadata to // containers. // Optional. Labels map[string]string `json:"labels,omitempty"` |