diff options
author | Josh Patterson <josh.patterson@securityonionsolutions.com> | 2022-08-29 15:48:02 -0400 |
---|---|---|
committer | Josh Patterson <josh.patterson@securityonionsolutions.com> | 2022-08-29 15:48:02 -0400 |
commit | 0e53c8c73509e666bbb5ff4ba0ec2a8fa5c8c1b8 (patch) | |
tree | 139b60bd14d06eaf9c6c0a40d78c1e3c08404037 /pkg | |
parent | 08af95f63576af0c443fdef9d3ba6ba12a0c0dbc (diff) | |
parent | 468aa6478c73e4acd8708ce8bb0bb5a056f329c2 (diff) | |
download | podman-0e53c8c73509e666bbb5ff4ba0ec2a8fa5c8c1b8.tar.gz podman-0e53c8c73509e666bbb5ff4ba0ec2a8fa5c8c1b8.tar.bz2 podman-0e53c8c73509e666bbb5ff4ba0ec2a8fa5c8c1b8.zip |
Merge remote-tracking branch 'upstream/main' into api_compat_containers
Diffstat (limited to 'pkg')
57 files changed, 1407 insertions, 455 deletions
diff --git a/pkg/api/handlers/compat/auth.go b/pkg/api/handlers/compat/auth.go index 37d2b784d..ee478b9e3 100644 --- a/pkg/api/handlers/compat/auth.go +++ b/pkg/api/handlers/compat/auth.go @@ -1,7 +1,6 @@ package compat import ( - "context" "encoding/json" "errors" "fmt" @@ -44,7 +43,7 @@ func Auth(w http.ResponseWriter, r *http.Request) { fmt.Println("Authenticating with existing credentials...") registry := stripAddressOfScheme(authConfig.ServerAddress) - if err := DockerClient.CheckAuth(context.Background(), sysCtx, authConfig.Username, authConfig.Password, registry); err == nil { + if err := DockerClient.CheckAuth(r.Context(), sysCtx, authConfig.Username, authConfig.Password, registry); err == nil { utils.WriteResponse(w, http.StatusOK, entities.AuthReport{ IdentityToken: "", Status: "Login Succeeded", diff --git a/pkg/api/handlers/compat/containers.go b/pkg/api/handlers/compat/containers.go index 35681cb36..61d6fc86d 100644 --- a/pkg/api/handlers/compat/containers.go +++ b/pkg/api/handlers/compat/containers.go @@ -467,6 +467,7 @@ func LibpodToContainerJSON(l *libpod.Container, sz bool) (*types.ContainerJSON, if err := json.Unmarshal(h, &hc); err != nil { return nil, err } + sort.Strings(hc.Binds) // k8s-file == json-file if hc.LogConfig.Type == define.KubernetesLogging { diff --git a/pkg/api/handlers/compat/containers_create.go b/pkg/api/handlers/compat/containers_create.go index 9fff8b4c8..d4f5d5f36 100644 --- a/pkg/api/handlers/compat/containers_create.go +++ b/pkg/api/handlers/compat/containers_create.go @@ -408,6 +408,7 @@ func cliOpts(cc handlers.CreateContainerConfig, rtc *config.Config) (*entities.C Systemd: "true", // podman default TmpFS: parsedTmp, TTY: cc.Config.Tty, + EnvMerge: cc.EnvMerge, UnsetEnv: cc.UnsetEnv, UnsetEnvAll: cc.UnsetEnvAll, User: cc.Config.User, diff --git a/pkg/api/handlers/compat/events.go b/pkg/api/handlers/compat/events.go index 18fb35966..105404a0d 100644 --- a/pkg/api/handlers/compat/events.go +++ b/pkg/api/handlers/compat/events.go @@ -89,6 +89,12 @@ func GetEvents(w http.ResponseWriter, r *http.Request) { } e := entities.ConvertToEntitiesEvent(*evt) + // Some events differ between Libpod and Docker endpoints. + // Handle these differences for Docker-compat. + if !utils.IsLibpodRequest(r) && e.Type == "image" && e.Status == "remove" { + e.Status = "delete" + e.Action = "delete" + } if !utils.IsLibpodRequest(r) && e.Status == "died" { e.Status = "die" e.Action = "die" diff --git a/pkg/api/handlers/compat/images_build.go b/pkg/api/handlers/compat/images_build.go index a00f0b089..7ba1029a7 100644 --- a/pkg/api/handlers/compat/images_build.go +++ b/pkg/api/handlers/compat/images_build.go @@ -101,6 +101,7 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { ForceRm bool `schema:"forcerm"` From string `schema:"from"` HTTPProxy bool `schema:"httpproxy"` + IDMappingOptions string `schema:"idmappingoptions"` IdentityLabel bool `schema:"identitylabel"` Ignore bool `schema:"ignore"` Isolation string `schema:"isolation"` @@ -389,6 +390,14 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { } } + var idMappingOptions buildahDefine.IDMappingOptions + if _, found := r.URL.Query()["idmappingoptions"]; found { + if err := json.Unmarshal([]byte(query.IDMappingOptions), &idMappingOptions); err != nil { + utils.BadRequest(w, "idmappingoptions", query.IDMappingOptions, err) + return + } + } + var cacheFrom reference.Named if _, found := r.URL.Query()["cachefrom"]; found { cacheFrom, err = parse.RepoNameToNamedReference(query.CacheFrom) @@ -644,6 +653,7 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { Excludes: excludes, ForceRmIntermediateCtrs: query.ForceRm, From: fromImage, + IDMappingOptions: &idMappingOptions, IgnoreUnrecognizedInstructions: query.Ignore, Isolation: isolation, Jobs: &jobs, @@ -694,7 +704,7 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { success bool ) - runCtx, cancel := context.WithCancel(context.Background()) + runCtx, cancel := context.WithCancel(r.Context()) go func() { defer cancel() imageID, _, err = runtime.Build(r.Context(), buildOptions, containerFiles...) diff --git a/pkg/api/handlers/libpod/containers_create.go b/pkg/api/handlers/libpod/containers_create.go index 1307c267a..429f45f91 100644 --- a/pkg/api/handlers/libpod/containers_create.go +++ b/pkg/api/handlers/libpod/containers_create.go @@ -1,7 +1,6 @@ package libpod import ( - "context" "encoding/json" "fmt" "net/http" @@ -63,12 +62,12 @@ func CreateContainer(w http.ResponseWriter, r *http.Request) { utils.InternalServerError(w, err) return } - rtSpec, spec, opts, err := generate.MakeContainer(context.Background(), runtime, &sg, false, nil) + rtSpec, spec, opts, err := generate.MakeContainer(r.Context(), runtime, &sg, false, nil) if err != nil { utils.InternalServerError(w, err) return } - ctr, err := generate.ExecuteCreate(context.Background(), runtime, rtSpec, spec, false, opts...) + ctr, err := generate.ExecuteCreate(r.Context(), runtime, rtSpec, spec, false, opts...) if err != nil { utils.InternalServerError(w, err) return diff --git a/pkg/api/handlers/libpod/images_pull.go b/pkg/api/handlers/libpod/images_pull.go index 7e24ae5ac..57b2e3a78 100644 --- a/pkg/api/handlers/libpod/images_pull.go +++ b/pkg/api/handlers/libpod/images_pull.go @@ -82,17 +82,32 @@ func ImagesPull(w http.ResponseWriter, r *http.Request) { pullOptions.IdentityToken = authConf.IdentityToken } - writer := channel.NewWriter(make(chan []byte)) - defer writer.Close() - - pullOptions.Writer = writer - pullPolicy, err := config.ParsePullPolicy(query.PullPolicy) if err != nil { utils.Error(w, http.StatusBadRequest, err) return } + // Let's keep thing simple when running in quiet mode and pull directly. + if query.Quiet { + images, err := runtime.LibimageRuntime().Pull(r.Context(), query.Reference, pullPolicy, pullOptions) + var report entities.ImagePullReport + if err != nil { + report.Error = err.Error() + } + for _, image := range images { + report.Images = append(report.Images, image.ID()) + // Pull last ID from list and publish in 'id' stanza. This maintains previous API contract + report.ID = image.ID() + } + utils.WriteResponse(w, http.StatusOK, report) + return + } + + writer := channel.NewWriter(make(chan []byte)) + defer writer.Close() + pullOptions.Writer = writer + var pulledImages []*libimage.Image var pullError error runCtx, cancel := context.WithCancel(r.Context()) @@ -118,10 +133,8 @@ func ImagesPull(w http.ResponseWriter, r *http.Request) { select { case s := <-writer.Chan(): report.Stream = string(s) - if !query.Quiet { - if err := enc.Encode(report); err != nil { - logrus.Warnf("Failed to encode json: %v", err) - } + if err := enc.Encode(report); err != nil { + logrus.Warnf("Failed to encode json: %v", err) } flush() case <-runCtx.Done(): diff --git a/pkg/api/handlers/libpod/images_push.go b/pkg/api/handlers/libpod/images_push.go index e931fd2f9..be6f5b131 100644 --- a/pkg/api/handlers/libpod/images_push.go +++ b/pkg/api/handlers/libpod/images_push.go @@ -90,7 +90,7 @@ func PushImage(w http.ResponseWriter, r *http.Request) { // Let's keep thing simple when running in quiet mode and push directly. if query.Quiet { - if err := imageEngine.Push(context.Background(), source, destination, options); err != nil { + if err := imageEngine.Push(r.Context(), source, destination, options); err != nil { utils.Error(w, http.StatusBadRequest, fmt.Errorf("error pushing image %q: %w", destination, err)) return } diff --git a/pkg/api/handlers/libpod/manifests.go b/pkg/api/handlers/libpod/manifests.go index b0c93f3b9..8391def5c 100644 --- a/pkg/api/handlers/libpod/manifests.go +++ b/pkg/api/handlers/libpod/manifests.go @@ -36,6 +36,7 @@ func ManifestCreate(w http.ResponseWriter, r *http.Request) { Name string `schema:"name"` Images []string `schema:"images"` All bool `schema:"all"` + Amend bool `schema:"amend"` }{ // Add defaults here once needed. } @@ -70,7 +71,7 @@ func ManifestCreate(w http.ResponseWriter, r *http.Request) { imageEngine := abi.ImageEngine{Libpod: runtime} - createOptions := entities.ManifestCreateOptions{All: query.All} + createOptions := entities.ManifestCreateOptions{All: query.All, Amend: query.Amend} manID, err := imageEngine.ManifestCreate(r.Context(), query.Name, query.Images, createOptions) if err != nil { utils.InternalServerError(w, err) @@ -292,7 +293,7 @@ func ManifestPushV3(w http.ResponseWriter, r *http.Request) { options.SkipTLSVerify = types.NewOptionalBool(!query.TLSVerify) } imageEngine := abi.ImageEngine{Libpod: runtime} - digest, err := imageEngine.ManifestPush(context.Background(), source, query.Destination, options) + digest, err := imageEngine.ManifestPush(r.Context(), source, query.Destination, options) if err != nil { utils.Error(w, http.StatusBadRequest, fmt.Errorf("error pushing image %q: %w", query.Destination, err)) return @@ -366,7 +367,7 @@ func ManifestPush(w http.ResponseWriter, r *http.Request) { // Let's keep thing simple when running in quiet mode and push directly. if query.Quiet { - digest, err := imageEngine.ManifestPush(context.Background(), source, destination, options) + digest, err := imageEngine.ManifestPush(r.Context(), source, destination, options) if err != nil { utils.Error(w, http.StatusBadRequest, fmt.Errorf("error pushing image %q: %w", destination, err)) return diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go index b533e131c..aab905878 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -127,6 +127,7 @@ type CreateContainerConfig struct { dockerContainer.Config // desired container configuration HostConfig dockerContainer.HostConfig // host dependent configuration for container NetworkingConfig dockerNetwork.NetworkingConfig // network configuration for container + EnvMerge []string // preprocess env variables from image before injecting into containers UnsetEnv []string // unset specified default environment variables UnsetEnvAll bool // unset all default environment variables } @@ -162,7 +163,7 @@ type ExecStartConfig struct { func ImageDataToImageInspect(ctx context.Context, l *libimage.Image) (*ImageInspect, error) { options := &libimage.InspectOptions{WithParent: true, WithSize: true} - info, err := l.Inspect(context.Background(), options) + info, err := l.Inspect(ctx, options) if err != nil { return nil, err } diff --git a/pkg/api/server/register_manifest.go b/pkg/api/server/register_manifest.go index c22479cf9..7a55eaefe 100644 --- a/pkg/api/server/register_manifest.go +++ b/pkg/api/server/register_manifest.go @@ -117,6 +117,10 @@ func (s *APIServer) registerManifestHandlers(r *mux.Router) error { // name: all // type: boolean // description: add all contents if given list + // - in: query + // name: amend + // type: boolean + // description: modify an existing list if one with the desired name already exists // - in: body // name: options // description: options for new manifest diff --git a/pkg/bindings/images/build.go b/pkg/bindings/images/build.go index 2615bc516..8348ac54b 100644 --- a/pkg/bindings/images/build.go +++ b/pkg/bindings/images/build.go @@ -88,6 +88,13 @@ func Build(ctx context.Context, containerFiles []string, options entities.BuildO } params.Set("additionalbuildcontexts", string(additionalBuildContextMap)) } + if options.IDMappingOptions != nil { + idmappingsOptions, err := jsoniter.Marshal(options.IDMappingOptions) + if err != nil { + return nil, err + } + params.Set("idmappingoptions", string(idmappingsOptions)) + } if buildArgs := options.Args; len(buildArgs) > 0 { bArgs, err := jsoniter.MarshalToString(buildArgs) if err != nil { diff --git a/pkg/bindings/images/pull.go b/pkg/bindings/images/pull.go index 1a4aa3038..109981c63 100644 --- a/pkg/bindings/images/pull.go +++ b/pkg/bindings/images/pull.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net/http" "os" "strconv" @@ -57,10 +56,14 @@ func Pull(ctx context.Context, rawImage string, options *PullOptions) ([]string, return nil, response.Process(err) } - // Historically pull writes status to stderr - stderr := io.Writer(os.Stderr) + var writer io.Writer if options.GetQuiet() { - stderr = ioutil.Discard + writer = io.Discard + } else if progressWriter := options.GetProgressWriter(); progressWriter != nil { + writer = progressWriter + } else { + // Historically push writes status to stderr + writer = os.Stderr } dec := json.NewDecoder(response.Body) @@ -84,7 +87,7 @@ func Pull(ctx context.Context, rawImage string, options *PullOptions) ([]string, switch { case report.Stream != "": - fmt.Fprint(stderr, report.Stream) + fmt.Fprint(writer, report.Stream) case report.Error != "": pullErrors = append(pullErrors, errors.New(report.Error)) case len(report.Images) > 0: diff --git a/pkg/bindings/images/push.go b/pkg/bindings/images/push.go index 5069dd780..f1e059f8c 100644 --- a/pkg/bindings/images/push.go +++ b/pkg/bindings/images/push.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net/http" "os" "strconv" @@ -58,12 +57,14 @@ func Push(ctx context.Context, source string, destination string, options *PushO return response.Process(err) } - // Historically push writes status to stderr - writer := io.Writer(os.Stderr) + var writer io.Writer if options.GetQuiet() { - writer = ioutil.Discard + writer = io.Discard } else if progressWriter := options.GetProgressWriter(); progressWriter != nil { writer = progressWriter + } else { + // Historically push writes status to stderr + writer = os.Stderr } dec := json.NewDecoder(response.Body) diff --git a/pkg/bindings/images/types.go b/pkg/bindings/images/types.go index 7b28c499e..3ecfb9e09 100644 --- a/pkg/bindings/images/types.go +++ b/pkg/bindings/images/types.go @@ -182,6 +182,8 @@ type PullOptions struct { Policy *string // Password for authenticating against the registry. Password *string + // ProgressWriter is a writer where pull progress are sent. + ProgressWriter *io.Writer // Quiet can be specified to suppress pull progress when pulling. Ignored // for remote calls. Quiet *bool diff --git a/pkg/bindings/images/types_pull_options.go b/pkg/bindings/images/types_pull_options.go index 4cd525185..c1a88fd9e 100644 --- a/pkg/bindings/images/types_pull_options.go +++ b/pkg/bindings/images/types_pull_options.go @@ -2,6 +2,7 @@ package images import ( + "io" "net/url" "github.com/containers/podman/v4/pkg/bindings/internal/util" @@ -107,6 +108,21 @@ func (o *PullOptions) GetPassword() string { return *o.Password } +// WithProgressWriter set field ProgressWriter to given value +func (o *PullOptions) WithProgressWriter(value io.Writer) *PullOptions { + o.ProgressWriter = &value + return o +} + +// GetProgressWriter returns value of field ProgressWriter +func (o *PullOptions) GetProgressWriter() io.Writer { + if o.ProgressWriter == nil { + var z io.Writer + return z + } + return *o.ProgressWriter +} + // WithQuiet set field Quiet to given value func (o *PullOptions) WithQuiet(value bool) *PullOptions { o.Quiet = &value diff --git a/pkg/bindings/manifests/manifests.go b/pkg/bindings/manifests/manifests.go index 49e4089f5..0163d21a0 100644 --- a/pkg/bindings/manifests/manifests.go +++ b/pkg/bindings/manifests/manifests.go @@ -182,12 +182,14 @@ func Push(ctx context.Context, name, destination string, options *images.PushOpt return "", response.Process(err) } - // Historically push writes status to stderr - writer := io.Writer(os.Stderr) + var writer io.Writer if options.GetQuiet() { writer = io.Discard } else if progressWriter := options.GetProgressWriter(); progressWriter != nil { writer = progressWriter + } else { + // Historically push writes status to stderr + writer = os.Stderr } dec := json.NewDecoder(response.Body) diff --git a/pkg/bindings/manifests/types.go b/pkg/bindings/manifests/types.go index e23ef798d..5f2557fe1 100644 --- a/pkg/bindings/manifests/types.go +++ b/pkg/bindings/manifests/types.go @@ -8,7 +8,8 @@ type InspectOptions struct { //go:generate go run ../generator/generator.go CreateOptions // CreateOptions are optional options for creating manifests type CreateOptions struct { - All *bool + All *bool + Amend *bool } //go:generate go run ../generator/generator.go ExistsOptions diff --git a/pkg/bindings/manifests/types_create_options.go b/pkg/bindings/manifests/types_create_options.go index 960332a82..09942c00a 100644 --- a/pkg/bindings/manifests/types_create_options.go +++ b/pkg/bindings/manifests/types_create_options.go @@ -31,3 +31,18 @@ func (o *CreateOptions) GetAll() bool { } return *o.All } + +// WithAmend set field Amend to given value +func (o *CreateOptions) WithAmend(value bool) *CreateOptions { + o.Amend = &value + return o +} + +// GetAmend returns value of field Amend +func (o *CreateOptions) GetAmend() bool { + if o.Amend == nil { + var z bool + return z + } + return *o.Amend +} diff --git a/pkg/bindings/test/images_test.go b/pkg/bindings/test/images_test.go index 9c9796661..53c5a1e83 100644 --- a/pkg/bindings/test/images_test.go +++ b/pkg/bindings/test/images_test.go @@ -1,11 +1,14 @@ package bindings_test import ( + "bytes" + "fmt" "net/http" "os" "path/filepath" "time" + podmanRegistry "github.com/containers/podman/v4/hack/podman-registry-go" "github.com/containers/podman/v4/pkg/bindings" "github.com/containers/podman/v4/pkg/bindings/containers" "github.com/containers/podman/v4/pkg/bindings/images" @@ -362,9 +365,14 @@ var _ = Describe("Podman images", func() { It("Image Pull", func() { rawImage := "docker.io/library/busybox:latest" - pulledImages, err := images.Pull(bt.conn, rawImage, nil) + var writer bytes.Buffer + pullOpts := new(images.PullOptions).WithProgressWriter(&writer) + pulledImages, err := images.Pull(bt.conn, rawImage, pullOpts) Expect(err).NotTo(HaveOccurred()) Expect(len(pulledImages)).To(Equal(1)) + output := writer.String() + Expect(output).To(ContainSubstring("Trying to pull ")) + Expect(output).To(ContainSubstring("Getting image source signatures")) exists, err := images.Exists(bt.conn, rawImage, nil) Expect(err).NotTo(HaveOccurred()) @@ -380,7 +388,19 @@ var _ = Describe("Podman images", func() { }) It("Image Push", func() { - Skip("TODO: implement test for image push to registry") + registry, err := podmanRegistry.Start() + Expect(err).To(BeNil()) + + var writer bytes.Buffer + pushOpts := new(images.PushOptions).WithUsername(registry.User).WithPassword(registry.Password).WithSkipTLSVerify(true).WithProgressWriter(&writer).WithQuiet(false) + err = images.Push(bt.conn, alpine.name, fmt.Sprintf("localhost:%s/test:latest", registry.Port), pushOpts) + Expect(err).ToNot(HaveOccurred()) + + output := writer.String() + Expect(output).To(ContainSubstring("Copying blob ")) + Expect(output).To(ContainSubstring("Copying config ")) + Expect(output).To(ContainSubstring("Writing manifest to image destination")) + Expect(output).To(ContainSubstring("Storing signatures")) }) It("Build no options", func() { diff --git a/pkg/bindings/test/manifests_test.go b/pkg/bindings/test/manifests_test.go index 6a34ef5a6..d6749f920 100644 --- a/pkg/bindings/test/manifests_test.go +++ b/pkg/bindings/test/manifests_test.go @@ -1,9 +1,12 @@ package bindings_test import ( + "bytes" + "fmt" "net/http" "time" + podmanRegistry "github.com/containers/podman/v4/hack/podman-registry-go" "github.com/containers/podman/v4/pkg/bindings" "github.com/containers/podman/v4/pkg/bindings/images" "github.com/containers/podman/v4/pkg/bindings/manifests" @@ -12,7 +15,7 @@ import ( "github.com/onsi/gomega/gexec" ) -var _ = Describe("podman manifest", func() { +var _ = Describe("Podman manifests", func() { var ( bt *bindingTest s *gexec.Session @@ -172,7 +175,21 @@ var _ = Describe("podman manifest", func() { Expect(list.Manifests[0].Platform.OS).To(Equal("foo")) }) - It("push manifest", func() { - Skip("TODO: implement test for manifest push to registry") + It("Manifest Push", func() { + registry, err := podmanRegistry.Start() + Expect(err).To(BeNil()) + + name := "quay.io/libpod/foobar:latest" + _, err = manifests.Create(bt.conn, name, []string{alpine.name}, nil) + Expect(err).ToNot(HaveOccurred()) + + var writer bytes.Buffer + pushOpts := new(images.PushOptions).WithUsername(registry.User).WithPassword(registry.Password).WithAll(true).WithSkipTLSVerify(true).WithProgressWriter(&writer).WithQuiet(false) + _, err = manifests.Push(bt.conn, name, fmt.Sprintf("localhost:%s/test:latest", registry.Port), pushOpts) + Expect(err).ToNot(HaveOccurred()) + + output := writer.String() + Expect(output).To(ContainSubstring("Writing manifest list to image destination")) + Expect(output).To(ContainSubstring("Storing list signatures")) }) }) diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go index 3ba507750..91ccdc2b2 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -119,6 +119,7 @@ type KillReport struct { } type RestartOptions struct { + Filters map[string][]string All bool Latest bool Running bool @@ -126,8 +127,9 @@ type RestartOptions struct { } type RestartReport struct { - Err error - Id string //nolint:revive,stylecheck + Err error + Id string //nolint:revive,stylecheck + RawInput string } type RmOptions struct { diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index 21c1372b9..cad11b0ab 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -156,6 +156,8 @@ type ImagePullOptions struct { SkipTLSVerify types.OptionalBool // PullPolicy whether to pull new image PullPolicy config.PullPolicy + // Writer is used to display copy information including progress bars. + Writer io.Writer } // ImagePullReport is the response from pulling one or more images. diff --git a/pkg/domain/entities/manifest.go b/pkg/domain/entities/manifest.go index 126b76c62..f17079271 100644 --- a/pkg/domain/entities/manifest.go +++ b/pkg/domain/entities/manifest.go @@ -4,7 +4,12 @@ import "github.com/containers/image/v5/types" // ManifestCreateOptions provides model for creating manifest type ManifestCreateOptions struct { + // True when adding lists to include all images All bool `schema:"all"` + // Amend an extant list if there's already one with the desired name + Amend bool `schema:"amend"` + // Should TLS registry certificate be verified? + SkipTLSVerify types.OptionalBool `json:"-" schema:"-"` } // ManifestAddOptions provides model for adding digests to manifest list diff --git a/pkg/domain/entities/pods.go b/pkg/domain/entities/pods.go index 14ce370c1..33ca2c807 100644 --- a/pkg/domain/entities/pods.go +++ b/pkg/domain/entities/pods.go @@ -263,6 +263,7 @@ type ContainerCreateOptions struct { TTY bool Timezone string Umask string + EnvMerge []string UnsetEnv []string UnsetEnvAll bool UIDMap []string diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index 5b5bc665e..0a8e5bc2f 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -309,31 +309,42 @@ func (ic *ContainerEngine) ContainerKill(ctx context.Context, namesOrIds []strin func (ic *ContainerEngine) ContainerRestart(ctx context.Context, namesOrIds []string, options entities.RestartOptions) ([]*entities.RestartReport, error) { var ( - ctrs []*libpod.Container - err error + ctrs []*libpod.Container + err error + rawInputs = []string{} ) if options.Running { ctrs, err = ic.Libpod.GetRunningContainers() + for _, candidate := range ctrs { + rawInputs = append(rawInputs, candidate.ID()) + } + if err != nil { return nil, err } } else { - ctrs, err = getContainersByContext(options.All, options.Latest, namesOrIds, ic.Libpod) + ctrs, rawInputs, err = getContainersAndInputByContext(options.All, options.Latest, namesOrIds, options.Filters, ic.Libpod) if err != nil { return nil, err } } - + idToRawInput := map[string]string{} + if len(rawInputs) == len(ctrs) { + for i := range ctrs { + idToRawInput[ctrs[i].ID()] = rawInputs[i] + } + } reports := make([]*entities.RestartReport, 0, len(ctrs)) - for _, con := range ctrs { - timeout := con.StopTimeout() + for _, c := range ctrs { + timeout := c.StopTimeout() if options.Timeout != nil { timeout = *options.Timeout } reports = append(reports, &entities.RestartReport{ - Id: con.ID(), - Err: con.RestartWithTimeout(ctx, timeout), + Id: c.ID(), + Err: c.RestartWithTimeout(ctx, timeout), + RawInput: idToRawInput[c.ID()], }) } return reports, nil @@ -370,7 +381,7 @@ func (ic *ContainerEngine) ContainerRm(ctx context.Context, namesOrIds []string, // this will fail and code will fall through to removing the container from libpod.` tmpNames := []string{} for _, ctr := range names { - report := reports.RmReport{Id: ctr, RawInput: ctr} + report := reports.RmReport{Id: ctr} report.Err = ic.Libpod.RemoveStorageContainer(ctr, options.Force) //nolint:gocritic if report.Err == nil { @@ -927,13 +938,15 @@ func (ic *ContainerEngine) ContainerStart(ctx context.Context, namesOrIds []stri if err != nil { return nil, err } + idToRawInput := map[string]string{} + if len(rawInputs) == len(ctrs) { + for i := range ctrs { + idToRawInput[ctrs[i].ID()] = rawInputs[i] + } + } // There can only be one container if attach was used for i := range ctrs { ctr := ctrs[i] - rawInput := ctr.ID() - if !options.All { - rawInput = rawInputs[i] - } ctrState, err := ctr.State() if err != nil { return nil, err @@ -947,7 +960,7 @@ func (ic *ContainerEngine) ContainerStart(ctx context.Context, namesOrIds []stri // Exit cleanly immediately reports = append(reports, &entities.ContainerStartReport{ Id: ctr.ID(), - RawInput: rawInput, + RawInput: idToRawInput[ctr.ID()], Err: nil, ExitCode: 0, }) @@ -958,7 +971,7 @@ func (ic *ContainerEngine) ContainerStart(ctx context.Context, namesOrIds []stri logrus.Debugf("Deadlock error: %v", err) reports = append(reports, &entities.ContainerStartReport{ Id: ctr.ID(), - RawInput: rawInput, + RawInput: idToRawInput[ctr.ID()], Err: err, ExitCode: define.ExitCode(err), }) @@ -968,7 +981,7 @@ func (ic *ContainerEngine) ContainerStart(ctx context.Context, namesOrIds []stri if ctrRunning { reports = append(reports, &entities.ContainerStartReport{ Id: ctr.ID(), - RawInput: rawInput, + RawInput: idToRawInput[ctr.ID()], Err: nil, ExitCode: 0, }) @@ -978,7 +991,7 @@ func (ic *ContainerEngine) ContainerStart(ctx context.Context, namesOrIds []stri if err != nil { reports = append(reports, &entities.ContainerStartReport{ Id: ctr.ID(), - RawInput: rawInput, + RawInput: idToRawInput[ctr.ID()], Err: err, ExitCode: exitCode, }) @@ -993,7 +1006,7 @@ func (ic *ContainerEngine) ContainerStart(ctx context.Context, namesOrIds []stri exitCode = ic.GetContainerExitCode(ctx, ctr) reports = append(reports, &entities.ContainerStartReport{ Id: ctr.ID(), - RawInput: rawInput, + RawInput: idToRawInput[ctr.ID()], Err: err, ExitCode: exitCode, }) @@ -1006,7 +1019,7 @@ func (ic *ContainerEngine) ContainerStart(ctx context.Context, namesOrIds []stri // If the container is in a pod, also set to recursively start dependencies report := &entities.ContainerStartReport{ Id: ctr.ID(), - RawInput: rawInput, + RawInput: idToRawInput[ctr.ID()], ExitCode: 125, } if err := ctr.Start(ctx, true); err != nil { diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index 77d1bf0db..f9839f62f 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -237,8 +237,9 @@ func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, options entiti pullOptions.Variant = options.Variant pullOptions.SignaturePolicyPath = options.SignaturePolicy pullOptions.InsecureSkipTLSVerify = options.SkipTLSVerify + pullOptions.Writer = options.Writer - if !options.Quiet { + if !options.Quiet && pullOptions.Writer == nil { pullOptions.Writer = os.Stderr } diff --git a/pkg/domain/infra/abi/manifest.go b/pkg/domain/infra/abi/manifest.go index e0c11267e..7e8c86526 100644 --- a/pkg/domain/infra/abi/manifest.go +++ b/pkg/domain/infra/abi/manifest.go @@ -32,7 +32,15 @@ func (ir *ImageEngine) ManifestCreate(ctx context.Context, name string, images [ manifestList, err := ir.Libpod.LibimageRuntime().CreateManifestList(name) if err != nil { - return "", err + if errors.Is(err, storage.ErrDuplicateName) && opts.Amend { + amendList, amendErr := ir.Libpod.LibimageRuntime().LookupManifestList(name) + if amendErr != nil { + return "", err + } + manifestList = amendList + } else { + return "", err + } } addOptions := &libimage.ManifestListAddOptions{All: opts.All} diff --git a/pkg/domain/infra/abi/parse/parse.go b/pkg/domain/infra/abi/parse/parse.go index 19699589b..fb2876bb2 100644 --- a/pkg/domain/infra/abi/parse/parse.go +++ b/pkg/domain/infra/abi/parse/parse.go @@ -86,8 +86,11 @@ func VolumeOptions(opts map[string]string) ([]libpod.VolumeCreateOption, error) if err != nil { return nil, fmt.Errorf("cannot convert Timeout %s to an integer: %w", splitO[1], err) } + if intTimeout < 0 { + return nil, fmt.Errorf("volume timeout cannot be negative (got %d)", intTimeout) + } logrus.Debugf("Removing timeout from options and adding WithTimeout for Timeout %d", intTimeout) - libpodOptions = append(libpodOptions, libpod.WithVolumeDriverTimeout(intTimeout)) + libpodOptions = append(libpodOptions, libpod.WithVolumeDriverTimeout(uint(intTimeout))) default: finalVal = append(finalVal, o) } diff --git a/pkg/domain/infra/abi/terminal/sigproxy_linux.go b/pkg/domain/infra/abi/terminal/sigproxy_commn.go index 16d345f06..3a0132ef3 100644 --- a/pkg/domain/infra/abi/terminal/sigproxy_linux.go +++ b/pkg/domain/infra/abi/terminal/sigproxy_commn.go @@ -1,3 +1,6 @@ +//go:build linux || freebsd +// +build linux freebsd + package terminal import ( diff --git a/pkg/domain/infra/abi/terminal/terminal_linux.go b/pkg/domain/infra/abi/terminal/terminal_common.go index 222590871..afae2c085 100644 --- a/pkg/domain/infra/abi/terminal/terminal_linux.go +++ b/pkg/domain/infra/abi/terminal/terminal_common.go @@ -1,3 +1,6 @@ +//go:build linux || freebsd +// +build linux freebsd + package terminal import ( diff --git a/pkg/domain/infra/abi/terminal/terminal_unsupported.go b/pkg/domain/infra/abi/terminal/terminal_unsupported.go index 8fe325736..21ed6c8d4 100644 --- a/pkg/domain/infra/abi/terminal/terminal_unsupported.go +++ b/pkg/domain/infra/abi/terminal/terminal_unsupported.go @@ -1,5 +1,5 @@ -//go:build !linux -// +build !linux +//go:build !linux && !freebsd +// +build !linux,!freebsd package terminal diff --git a/pkg/domain/infra/abi/trust.go b/pkg/domain/infra/abi/trust.go index 0e3d8fad9..c58ddff06 100644 --- a/pkg/domain/infra/abi/trust.go +++ b/pkg/domain/infra/abi/trust.go @@ -2,16 +2,11 @@ package abi import ( "context" - "encoding/json" - "errors" "fmt" "io/ioutil" - "os" - "strings" "github.com/containers/podman/v4/pkg/domain/entities" "github.com/containers/podman/v4/pkg/trust" - "github.com/sirupsen/logrus" ) func (ir *ImageEngine) ShowTrust(ctx context.Context, args []string, options entities.ShowTrustOptions) (*entities.ShowTrustReport, error) { @@ -34,11 +29,7 @@ func (ir *ImageEngine) ShowTrust(ctx context.Context, args []string, options ent if len(options.RegistryPath) > 0 { report.SystemRegistriesDirPath = options.RegistryPath } - policyContentStruct, err := trust.GetPolicy(policyPath) - if err != nil { - return nil, fmt.Errorf("could not read trust policies: %w", err) - } - report.Policies, err = getPolicyShowOutput(policyContentStruct, report.SystemRegistriesDirPath) + report.Policies, err = trust.PolicyDescription(policyPath, report.SystemRegistriesDirPath) if err != nil { return nil, fmt.Errorf("could not show trust policies: %w", err) } @@ -46,133 +37,19 @@ func (ir *ImageEngine) ShowTrust(ctx context.Context, args []string, options ent } func (ir *ImageEngine) SetTrust(ctx context.Context, args []string, options entities.SetTrustOptions) error { - var ( - policyContentStruct trust.PolicyContent - newReposContent []trust.RepoContent - ) - trustType := options.Type - if trustType == "accept" { - trustType = "insecureAcceptAnything" - } - - pubkeysfile := options.PubKeysFile - if len(pubkeysfile) == 0 && trustType == "signedBy" { - return errors.New("at least one public key must be defined for type 'signedBy'") + if len(args) != 1 { + return fmt.Errorf("SetTrust called with unexpected %d args", len(args)) } + scope := args[0] policyPath := trust.DefaultPolicyPath(ir.Libpod.SystemContext()) if len(options.PolicyPath) > 0 { policyPath = options.PolicyPath } - _, err := os.Stat(policyPath) - if !os.IsNotExist(err) { - policyContent, err := ioutil.ReadFile(policyPath) - if err != nil { - return err - } - if err := json.Unmarshal(policyContent, &policyContentStruct); err != nil { - return errors.New("could not read trust policies") - } - } - if len(pubkeysfile) != 0 { - for _, filepath := range pubkeysfile { - newReposContent = append(newReposContent, trust.RepoContent{Type: trustType, KeyType: "GPGKeys", KeyPath: filepath}) - } - } else { - newReposContent = append(newReposContent, trust.RepoContent{Type: trustType}) - } - if args[0] == "default" { - policyContentStruct.Default = newReposContent - } else { - if len(policyContentStruct.Default) == 0 { - return errors.New("default trust policy must be set") - } - registryExists := false - for transport, transportval := range policyContentStruct.Transports { - _, registryExists = transportval[args[0]] - if registryExists { - policyContentStruct.Transports[transport][args[0]] = newReposContent - break - } - } - if !registryExists { - if policyContentStruct.Transports == nil { - policyContentStruct.Transports = make(map[string]trust.RepoMap) - } - if policyContentStruct.Transports["docker"] == nil { - policyContentStruct.Transports["docker"] = make(map[string][]trust.RepoContent) - } - policyContentStruct.Transports["docker"][args[0]] = append(policyContentStruct.Transports["docker"][args[0]], newReposContent...) - } - } - - data, err := json.MarshalIndent(policyContentStruct, "", " ") - if err != nil { - return fmt.Errorf("error setting trust policy: %w", err) - } - return ioutil.WriteFile(policyPath, data, 0644) -} - -func getPolicyShowOutput(policyContentStruct trust.PolicyContent, systemRegistriesDirPath string) ([]*trust.Policy, error) { - var output []*trust.Policy - - registryConfigs, err := trust.LoadAndMergeConfig(systemRegistriesDirPath) - if err != nil { - return nil, err - } - - if len(policyContentStruct.Default) > 0 { - defaultPolicyStruct := trust.Policy{ - Transport: "all", - Name: "* (default)", - RepoName: "default", - Type: trustTypeDescription(policyContentStruct.Default[0].Type), - } - output = append(output, &defaultPolicyStruct) - } - for transport, transval := range policyContentStruct.Transports { - if transport == "docker" { - transport = "repository" - } - for repo, repoval := range transval { - tempTrustShowOutput := trust.Policy{ - Name: repo, - RepoName: repo, - Transport: transport, - Type: trustTypeDescription(repoval[0].Type), - } - // TODO - keyarr is not used and I don't know its intent; commenting out for now for someone to fix later - // keyarr := []string{} - uids := []string{} - for _, repoele := range repoval { - if len(repoele.KeyPath) > 0 { - // keyarr = append(keyarr, repoele.KeyPath) - uids = append(uids, trust.GetGPGIdFromKeyPath(repoele.KeyPath)...) - } - if len(repoele.KeyData) > 0 { - // keyarr = append(keyarr, string(repoele.KeyData)) - uids = append(uids, trust.GetGPGIdFromKeyData(repoele.KeyData)...) - } - } - tempTrustShowOutput.GPGId = strings.Join(uids, ", ") - - registryNamespace := trust.HaveMatchRegistry(repo, registryConfigs) - if registryNamespace != nil { - tempTrustShowOutput.SignatureStore = registryNamespace.SigStore - } - output = append(output, &tempTrustShowOutput) - } - } - return output, nil -} - -var typeDescription = map[string]string{"insecureAcceptAnything": "accept", "signedBy": "signed", "reject": "reject"} - -func trustTypeDescription(trustType string) string { - trustDescription, exist := typeDescription[trustType] - if !exist { - logrus.Warnf("Invalid trust type %s", trustType) - } - return trustDescription + return trust.AddPolicyEntries(policyPath, trust.AddPolicyEntriesInput{ + Scope: scope, + Type: options.Type, + PubKeyFiles: options.PubKeysFile, + }) } diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index d49f029d5..023bee430 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -183,17 +183,22 @@ func (ic *ContainerEngine) ContainerRestart(ctx context.Context, namesOrIds []st if to := opts.Timeout; to != nil { options.WithTimeout(int(*to)) } - ctrs, err := getContainersByContext(ic.ClientCtx, opts.All, false, namesOrIds) + ctrs, rawInputs, err := getContainersAndInputByContext(ic.ClientCtx, opts.All, false, namesOrIds, opts.Filters) if err != nil { return nil, err } + idToRawInput := map[string]string{} + for i := range ctrs { + idToRawInput[ctrs[i].ID] = rawInputs[i] + } for _, c := range ctrs { if opts.Running && c.State != define.ContainerStateRunning.String() { continue } reports = append(reports, &entities.RestartReport{ - Id: c.ID, - Err: containers.Restart(ic.ClientCtx, c.ID, options), + Id: c.ID, + Err: containers.Restart(ic.ClientCtx, c.ID, options), + RawInput: idToRawInput[c.ID], }) } return reports, nil @@ -667,10 +672,16 @@ func logIfRmError(id string, err error, reports []*reports.RmReport) { func (ic *ContainerEngine) ContainerStart(ctx context.Context, namesOrIds []string, options entities.ContainerStartOptions) ([]*entities.ContainerStartReport, error) { reports := []*entities.ContainerStartReport{} var exitCode = define.ExecErrorCodeGeneric - ctrs, namesOrIds, err := getContainersAndInputByContext(ic.ClientCtx, options.All, false, namesOrIds, options.Filters) + ctrs, rawInputs, err := getContainersAndInputByContext(ic.ClientCtx, options.All, false, namesOrIds, options.Filters) if err != nil { return nil, err } + idToRawInput := map[string]string{} + if len(rawInputs) == len(ctrs) { + for i := range ctrs { + idToRawInput[ctrs[i].ID] = rawInputs[i] + } + } removeOptions := new(containers.RemoveOptions).WithVolumes(true).WithForce(false) removeContainer := func(id string) { reports, err := containers.Remove(ic.ClientCtx, id, removeOptions) @@ -678,15 +689,11 @@ func (ic *ContainerEngine) ContainerStart(ctx context.Context, namesOrIds []stri } // There can only be one container if attach was used - for i, ctr := range ctrs { + for _, ctr := range ctrs { name := ctr.ID - rawInput := ctr.ID - if !options.All { - rawInput = namesOrIds[i] - } report := entities.ContainerStartReport{ Id: name, - RawInput: rawInput, + RawInput: idToRawInput[name], ExitCode: exitCode, } ctrRunning := ctr.State == define.ContainerStateRunning.String() diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go index bb3014099..2716aaf2a 100644 --- a/pkg/domain/infra/tunnel/images.go +++ b/pkg/domain/infra/tunnel/images.go @@ -110,6 +110,7 @@ func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, opts entities. options.WithAllTags(opts.AllTags).WithAuthfile(opts.Authfile).WithArch(opts.Arch).WithOS(opts.OS) options.WithVariant(opts.Variant).WithPassword(opts.Password) options.WithQuiet(opts.Quiet).WithUsername(opts.Username).WithPolicy(opts.PullPolicy.String()) + options.WithProgressWriter(opts.Writer) if s := opts.SkipTLSVerify; s != types.OptionalBoolUndefined { if s == types.OptionalBoolTrue { options.WithSkipTLSVerify(true) diff --git a/pkg/domain/infra/tunnel/manifest.go b/pkg/domain/infra/tunnel/manifest.go index 2a514861d..2e6134051 100644 --- a/pkg/domain/infra/tunnel/manifest.go +++ b/pkg/domain/infra/tunnel/manifest.go @@ -15,7 +15,7 @@ import ( // ManifestCreate implements manifest create via ImageEngine func (ir *ImageEngine) ManifestCreate(ctx context.Context, name string, images []string, opts entities.ManifestCreateOptions) (string, error) { - options := new(manifests.CreateOptions).WithAll(opts.All) + options := new(manifests.CreateOptions).WithAll(opts.All).WithAmend(opts.Amend) imageID, err := manifests.Create(ir.ClientCtx, name, images, options) if err != nil { return imageID, fmt.Errorf("error creating manifest: %w", err) diff --git a/pkg/env/env.go b/pkg/env/env.go index 8af9fd77c..fb7949ad8 100644 --- a/pkg/env/env.go +++ b/pkg/env/env.go @@ -37,6 +37,22 @@ func Slice(m map[string]string) []string { return env } +// Map transforms the specified slice of environment variables into a +// map. +func Map(slice []string) map[string]string { + envmap := make(map[string]string, len(slice)) + for _, val := range slice { + data := strings.SplitN(val, "=", 2) + + if len(data) > 1 { + envmap[data[0]] = data[1] + } else { + envmap[data[0]] = "" + } + } + return envmap +} + // Join joins the two environment maps with override overriding base. func Join(base map[string]string, override map[string]string) map[string]string { if len(base) == 0 { @@ -87,10 +103,6 @@ func parseEnv(env map[string]string, line string) error { } // trim the front of a variable, but nothing else name := strings.TrimLeft(data[0], whiteSpaces) - if strings.ContainsAny(name, whiteSpaces) { - return fmt.Errorf("name %q has white spaces, poorly formatted name", name) - } - if len(data) > 1 { env[name] = data[1] } else { diff --git a/pkg/machine/config.go b/pkg/machine/config.go index 5162006db..54aa9e1b7 100644 --- a/pkg/machine/config.go +++ b/pkg/machine/config.go @@ -175,7 +175,7 @@ func (rc RemoteConnectionType) MakeSSHURL(host, path, port, userName string) url return uri } -// GetCacheDir returns the dir where VM images are downladed into when pulled +// GetCacheDir returns the dir where VM images are downloaded into when pulled func GetCacheDir(vmType string) (string, error) { dataDir, err := GetDataDir(vmType) if err != nil { diff --git a/pkg/machine/e2e/basic_test.go b/pkg/machine/e2e/basic_test.go index da0310485..fa1728770 100644 --- a/pkg/machine/e2e/basic_test.go +++ b/pkg/machine/e2e/basic_test.go @@ -1,6 +1,8 @@ package e2e_test import ( + "os" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" . "github.com/onsi/gomega/gexec" @@ -20,6 +22,12 @@ var _ = Describe("run basic podman commands", func() { }) It("Basic ops", func() { + // golangci-lint has trouble with actually skipping tests marked Skip + // so skip it on cirrus envs and where CIRRUS_CI isn't set. + if os.Getenv("CIRRUS_CI") != "false" { + Skip("FIXME: #15347 - ssh know hosts broken - fails on PR runs and on x86_64") + } + name := randomString() i := new(initMachine) session, err := mb.setName(name).setCmd(i.withImagePath(mb.imagePath).withNow()).run() diff --git a/pkg/machine/pull.go b/pkg/machine/pull.go index 26b6adc67..22a1b4c0a 100644 --- a/pkg/machine/pull.go +++ b/pkg/machine/pull.go @@ -213,8 +213,8 @@ func decompressXZ(src string, output io.WriteCloser) error { var read io.Reader var cmd *exec.Cmd // Prefer xz utils for fastest performance, fallback to go xi2 impl - if _, err := exec.LookPath("xzcat"); err == nil { - cmd = exec.Command("xzcat", "-k", src) + if _, err := exec.LookPath("xz"); err == nil { + cmd = exec.Command("xz", "-d", "-c", "-k", src) read, err = cmd.StdoutPipe() if err != nil { return err diff --git a/pkg/machine/qemu/claim_unsupported.go b/pkg/machine/qemu/claim_unsupported.go index e0b3dd3d3..187ef9d69 100644 --- a/pkg/machine/qemu/claim_unsupported.go +++ b/pkg/machine/qemu/claim_unsupported.go @@ -1,5 +1,5 @@ -//go:build !darwin && !windows -// +build !darwin,!windows +//go:build !darwin +// +build !darwin package qemu diff --git a/pkg/machine/qemu/config.go b/pkg/machine/qemu/config.go index bada1af9b..8081727f6 100644 --- a/pkg/machine/qemu/config.go +++ b/pkg/machine/qemu/config.go @@ -1,6 +1,3 @@ -//go:build (amd64 && !windows) || (arm64 && !windows) -// +build amd64,!windows arm64,!windows - package qemu import ( diff --git a/pkg/machine/qemu/machine.go b/pkg/machine/qemu/machine.go index 213f7ce5d..e97b68e31 100644 --- a/pkg/machine/qemu/machine.go +++ b/pkg/machine/qemu/machine.go @@ -545,12 +545,12 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { return err } defer fd.Close() - dnr, err := os.OpenFile("/dev/null", os.O_RDONLY, 0755) + dnr, err := os.OpenFile(os.DevNull, os.O_RDONLY, 0755) if err != nil { return err } defer dnr.Close() - dnw, err := os.OpenFile("/dev/null", os.O_WRONLY, 0755) + dnw, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0755) if err != nil { return err } @@ -870,7 +870,7 @@ func NewQMPMonitor(network, name string, timeout time.Duration) (Monitor, error) if err != nil { return Monitor{}, err } - if !rootless.IsRootless() { + if isRootful() { rtDir = "/run" } rtDir = filepath.Join(rtDir, "podman") @@ -1216,11 +1216,11 @@ func (v *MachineVM) startHostNetworking() (string, apiForwardingState, error) { } attr := new(os.ProcAttr) - dnr, err := os.OpenFile("/dev/null", os.O_RDONLY, 0755) + dnr, err := os.OpenFile(os.DevNull, os.O_RDONLY, 0755) if err != nil { return "", noForwarding, err } - dnw, err := os.OpenFile("/dev/null", os.O_WRONLY, 0755) + dnw, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0755) if err != nil { return "", noForwarding, err } @@ -1371,7 +1371,7 @@ func (v *MachineVM) setPIDSocket() error { if err != nil { return err } - if !rootless.IsRootless() { + if isRootful() { rtPath = "/run" } socketDir := filepath.Join(rtPath, "podman") @@ -1397,7 +1397,7 @@ func (v *MachineVM) getSocketandPid() (string, string, error) { if err != nil { return "", "", err } - if !rootless.IsRootless() { + if isRootful() { rtPath = "/run" } socketDir := filepath.Join(rtPath, "podman") @@ -1735,3 +1735,11 @@ func isProcessAlive(pid int) bool { func (p *Provider) VMType() string { return vmtype } + +func isRootful() bool { + // Rootless is not relevant on Windows. In the future rootless.IsRootless + // could be switched to return true on Windows, and other codepaths migrated + // for now will check additionally for valid os.Getuid + + return !rootless.IsRootless() && os.Getuid() != -1 +} diff --git a/pkg/specgen/generate/container.go b/pkg/specgen/generate/container.go index ec85f0f79..d57efa0d1 100644 --- a/pkg/specgen/generate/container.go +++ b/pkg/specgen/generate/container.go @@ -19,6 +19,7 @@ import ( "github.com/containers/podman/v4/pkg/signal" "github.com/containers/podman/v4/pkg/specgen" spec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/openshift/imagebuilder" "github.com/sirupsen/logrus" "golang.org/x/sys/unix" ) @@ -131,6 +132,17 @@ func CompleteSpec(ctx context.Context, r *libpod.Runtime, s *specgen.SpecGenerat defaultEnvs = envLib.Join(envLib.DefaultEnvVariables(), envLib.Join(defaultEnvs, envs)) } + for _, e := range s.EnvMerge { + processedWord, err := imagebuilder.ProcessWord(e, envLib.Slice(defaultEnvs)) + if err != nil { + return nil, fmt.Errorf("unable to process variables for --env-merge %s: %w", e, err) + } + splitWord := strings.Split(processedWord, "=") + if _, ok := defaultEnvs[splitWord[0]]; ok { + defaultEnvs[splitWord[0]] = splitWord[1] + } + } + for _, e := range s.UnsetEnv { delete(defaultEnvs, e) } @@ -140,10 +152,8 @@ func CompleteSpec(ctx context.Context, r *libpod.Runtime, s *specgen.SpecGenerat } // First transform the os env into a map. We need it for the labels later in // any case. - osEnv, err := envLib.ParseSlice(os.Environ()) - if err != nil { - return nil, fmt.Errorf("error parsing host environment variables: %w", err) - } + osEnv := envLib.Map(os.Environ()) + // Caller Specified defaults if s.EnvHost { defaultEnvs = envLib.Join(defaultEnvs, osEnv) @@ -347,9 +357,21 @@ func ConfigToSpec(rt *libpod.Runtime, specg *specgen.SpecGenerator, contaierID s conf.Systemd = tmpSystemd conf.Mounts = tmpMounts - if conf.Spec != nil && conf.Spec.Linux != nil && conf.Spec.Linux.Resources != nil { - if specg.ResourceLimits == nil { - specg.ResourceLimits = conf.Spec.Linux.Resources + if conf.Spec != nil { + if conf.Spec.Linux != nil && conf.Spec.Linux.Resources != nil { + if specg.ResourceLimits == nil { + specg.ResourceLimits = conf.Spec.Linux.Resources + } + } + if conf.Spec.Process != nil && conf.Spec.Process.Env != nil { + env := make(map[string]string) + for _, entry := range conf.Spec.Process.Env { + split := strings.Split(entry, "=") + if len(split) == 2 { + env[split[0]] = split[1] + } + } + specg.Env = env } } diff --git a/pkg/specgen/generate/validate.go b/pkg/specgen/generate/validate.go index 9c933d747..3c5d5fb96 100644 --- a/pkg/specgen/generate/validate.go +++ b/pkg/specgen/generate/validate.go @@ -9,6 +9,7 @@ import ( "github.com/containers/common/pkg/cgroups" "github.com/containers/common/pkg/sysinfo" + "github.com/containers/podman/v4/pkg/rootless" "github.com/containers/podman/v4/pkg/specgen" "github.com/containers/podman/v4/utils" ) @@ -19,6 +20,11 @@ func verifyContainerResourcesCgroupV1(s *specgen.SpecGenerator) ([]string, error sysInfo := sysinfo.New(true) + if s.ResourceLimits != nil && rootless.IsRootless() { + s.ResourceLimits = nil + warnings = append(warnings, "Resource limits are not supported and ignored on cgroups V1 rootless systems") + } + if s.ResourceLimits == nil { return warnings, nil } diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index b90f07ef8..51b6736a9 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -204,6 +204,9 @@ type ContainerBasicConfig struct { // The execution domain system allows Linux to provide limited support // for binaries compiled under other UNIX-like operating systems. Personality *spec.LinuxPersonality `json:"personality,omitempty"` + // EnvMerge takes the specified environment variables from image and preprocess them before injecting them into the + // container. + EnvMerge []string `json:"envmerge,omitempty"` // UnsetEnv unsets the specified default environment variables from the image or from buildin or containers.conf // Optional. UnsetEnv []string `json:"unsetenv,omitempty"` diff --git a/pkg/specgenutil/specgen.go b/pkg/specgenutil/specgen.go index 7392e7b44..8c2c59fed 100644 --- a/pkg/specgenutil/specgen.go +++ b/pkg/specgenutil/specgen.go @@ -362,10 +362,7 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions // First transform the os env into a map. We need it for the labels later in // any case. - osEnv, err := envLib.ParseSlice(os.Environ()) - if err != nil { - return fmt.Errorf("error parsing host environment variables: %w", err) - } + osEnv := envLib.Map(os.Environ()) if !s.EnvHost { s.EnvHost = c.EnvHost @@ -839,6 +836,9 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions if !s.Volatile { s.Volatile = c.Rm } + if len(s.EnvMerge) == 0 || len(c.EnvMerge) != 0 { + s.EnvMerge = c.EnvMerge + } if len(s.UnsetEnv) == 0 || len(c.UnsetEnv) != 0 { s.UnsetEnv = c.UnsetEnv } diff --git a/pkg/systemd/notifyproxy/notifyproxy_test.go b/pkg/systemd/notifyproxy/notifyproxy_test.go index edad95659..ce63fc9cd 100644 --- a/pkg/systemd/notifyproxy/notifyproxy_test.go +++ b/pkg/systemd/notifyproxy/notifyproxy_test.go @@ -37,7 +37,7 @@ func TestWaitAndClose(t *testing.T) { time.Sleep(250 * time.Millisecond) select { case err := <-ch: - t.Fatalf("Should stil be waiting but received %v", err) + t.Fatalf("Should still be waiting but received %v", err) default: } diff --git a/pkg/trust/config.go b/pkg/trust/config.go deleted file mode 100644 index 6186d4cbd..000000000 --- a/pkg/trust/config.go +++ /dev/null @@ -1,12 +0,0 @@ -package trust - -// Policy describes a basic trust policy configuration -type Policy struct { - Transport string `json:"transport"` - Name string `json:"name,omitempty"` - RepoName string `json:"repo_name,omitempty"` - Keys []string `json:"keys,omitempty"` - SignatureStore string `json:"sigstore,omitempty"` - Type string `json:"type"` - GPGId string `json:"gpg_id,omitempty"` -} diff --git a/pkg/trust/policy.go b/pkg/trust/policy.go new file mode 100644 index 000000000..326fe17af --- /dev/null +++ b/pkg/trust/policy.go @@ -0,0 +1,248 @@ +package trust + +import ( + "bufio" + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/containers/image/v5/types" + "github.com/sirupsen/logrus" +) + +// policyContent is the overall structure of a policy.json file (= c/image/v5/signature.Policy) +type policyContent struct { + Default []repoContent `json:"default"` + Transports transportsContent `json:"transports,omitempty"` +} + +// transportsContent contains policies for individual transports (= c/image/v5/signature.Policy.Transports) +type transportsContent map[string]repoMap + +// repoMap maps a scope name to requirements that apply to that scope (= c/image/v5/signature.PolicyTransportScopes) +type repoMap map[string][]repoContent + +// repoContent is a single policy requirement (one of possibly several for a scope), representing all of the individual alternatives in a single merged struct +// (= c/image/v5/signature.{PolicyRequirement,pr*}) +type repoContent struct { + Type string `json:"type"` + KeyType string `json:"keyType,omitempty"` + KeyPath string `json:"keyPath,omitempty"` + KeyPaths []string `json:"keyPaths,omitempty"` + KeyData string `json:"keyData,omitempty"` + SignedIdentity json.RawMessage `json:"signedIdentity,omitempty"` +} + +// genericPolicyContent is the overall structure of a policy.json file (= c/image/v5/signature.Policy), using generic data for individual requirements. +type genericPolicyContent struct { + Default json.RawMessage `json:"default"` + Transports genericTransportsContent `json:"transports,omitempty"` +} + +// genericTransportsContent contains policies for individual transports (= c/image/v5/signature.Policy.Transports), using generic data for individual requirements. +type genericTransportsContent map[string]genericRepoMap + +// genericRepoMap maps a scope name to requirements that apply to that scope (= c/image/v5/signature.PolicyTransportScopes) +type genericRepoMap map[string]json.RawMessage + +// DefaultPolicyPath returns a path to the default policy of the system. +func DefaultPolicyPath(sys *types.SystemContext) string { + systemDefaultPolicyPath := "/etc/containers/policy.json" + if sys != nil { + if sys.SignaturePolicyPath != "" { + return sys.SignaturePolicyPath + } + if sys.RootForImplicitAbsolutePaths != "" { + return filepath.Join(sys.RootForImplicitAbsolutePaths, systemDefaultPolicyPath) + } + } + return systemDefaultPolicyPath +} + +// gpgIDReader returns GPG key IDs of keys stored at the provided path. +// It exists only for tests, production code should always use getGPGIdFromKeyPath. +type gpgIDReader func(string) []string + +// createTmpFile creates a temp file under dir and writes the content into it +func createTmpFile(dir, pattern string, content []byte) (string, error) { + tmpfile, err := ioutil.TempFile(dir, pattern) + if err != nil { + return "", err + } + defer tmpfile.Close() + + if _, err := tmpfile.Write(content); err != nil { + return "", err + } + return tmpfile.Name(), nil +} + +// getGPGIdFromKeyPath returns GPG key IDs of keys stored at the provided path. +func getGPGIdFromKeyPath(path string) []string { + cmd := exec.Command("gpg2", "--with-colons", path) + results, err := cmd.Output() + if err != nil { + logrus.Errorf("Getting key identity: %s", err) + return nil + } + return parseUids(results) +} + +// getGPGIdFromKeyData returns GPG key IDs of keys in the provided keyring. +func getGPGIdFromKeyData(idReader gpgIDReader, key string) []string { + decodeKey, err := base64.StdEncoding.DecodeString(key) + if err != nil { + logrus.Errorf("%s, error decoding key data", err) + return nil + } + tmpfileName, err := createTmpFile("", "", decodeKey) + if err != nil { + logrus.Errorf("Creating key date temp file %s", err) + } + defer os.Remove(tmpfileName) + return idReader(tmpfileName) +} + +func parseUids(colonDelimitKeys []byte) []string { + var parseduids []string + scanner := bufio.NewScanner(bytes.NewReader(colonDelimitKeys)) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "uid:") || strings.HasPrefix(line, "pub:") { + uid := strings.Split(line, ":")[9] + if uid == "" { + continue + } + parseduid := uid + if strings.Contains(uid, "<") && strings.Contains(uid, ">") { + parseduid = strings.SplitN(strings.SplitAfterN(uid, "<", 2)[1], ">", 2)[0] + } + parseduids = append(parseduids, parseduid) + } + } + return parseduids +} + +// getPolicy parses policy.json into policyContent. +func getPolicy(policyPath string) (policyContent, error) { + var policyContentStruct policyContent + policyContent, err := ioutil.ReadFile(policyPath) + if err != nil { + return policyContentStruct, fmt.Errorf("unable to read policy file: %w", err) + } + if err := json.Unmarshal(policyContent, &policyContentStruct); err != nil { + return policyContentStruct, fmt.Errorf("could not parse trust policies from %s: %w", policyPath, err) + } + return policyContentStruct, nil +} + +var typeDescription = map[string]string{"insecureAcceptAnything": "accept", "signedBy": "signed", "sigstoreSigned": "sigstoreSigned", "reject": "reject"} + +func trustTypeDescription(trustType string) string { + trustDescription, exist := typeDescription[trustType] + if !exist { + logrus.Warnf("Invalid trust type %s", trustType) + } + return trustDescription +} + +// AddPolicyEntriesInput collects some parameters to AddPolicyEntries, +// primarily so that the callers use named values instead of just strings in a sequence. +type AddPolicyEntriesInput struct { + Scope string // "default" or a docker/atomic scope name + Type string + PubKeyFiles []string // For signature enforcement types, paths to public keys files (where the image needs to be signed by at least one key from _each_ of the files). File format depends on Type. +} + +// AddPolicyEntries adds one or more policy entries necessary to implement AddPolicyEntriesInput. +func AddPolicyEntries(policyPath string, input AddPolicyEntriesInput) error { + var ( + policyContentStruct genericPolicyContent + newReposContent []repoContent + ) + trustType := input.Type + if trustType == "accept" { + trustType = "insecureAcceptAnything" + } + pubkeysfile := input.PubKeyFiles + + // The error messages in validation failures use input.Type instead of trustType to match the user’s input. + switch trustType { + case "insecureAcceptAnything", "reject": + if len(pubkeysfile) != 0 { + return fmt.Errorf("%d public keys unexpectedly provided for trust type %v", len(pubkeysfile), input.Type) + } + newReposContent = append(newReposContent, repoContent{Type: trustType}) + + case "signedBy": + if len(pubkeysfile) == 0 { + return errors.New("at least one public key must be defined for type 'signedBy'") + } + for _, filepath := range pubkeysfile { + newReposContent = append(newReposContent, repoContent{Type: trustType, KeyType: "GPGKeys", KeyPath: filepath}) + } + + case "sigstoreSigned": + if len(pubkeysfile) == 0 { + return errors.New("at least one public key must be defined for type 'sigstoreSigned'") + } + for _, filepath := range pubkeysfile { + newReposContent = append(newReposContent, repoContent{Type: trustType, KeyPath: filepath}) + } + + default: + return fmt.Errorf("unknown trust type %q", input.Type) + } + newReposJSON, err := json.Marshal(newReposContent) + if err != nil { + return err + } + + _, err = os.Stat(policyPath) + if !os.IsNotExist(err) { + policyContent, err := ioutil.ReadFile(policyPath) + if err != nil { + return err + } + if err := json.Unmarshal(policyContent, &policyContentStruct); err != nil { + return errors.New("could not read trust policies") + } + } + if input.Scope == "default" { + policyContentStruct.Default = json.RawMessage(newReposJSON) + } else { + if len(policyContentStruct.Default) == 0 { + return errors.New("default trust policy must be set") + } + registryExists := false + for transport, transportval := range policyContentStruct.Transports { + _, registryExists = transportval[input.Scope] + if registryExists { + policyContentStruct.Transports[transport][input.Scope] = json.RawMessage(newReposJSON) + break + } + } + if !registryExists { + if policyContentStruct.Transports == nil { + policyContentStruct.Transports = make(map[string]genericRepoMap) + } + if policyContentStruct.Transports["docker"] == nil { + policyContentStruct.Transports["docker"] = make(map[string]json.RawMessage) + } + policyContentStruct.Transports["docker"][input.Scope] = json.RawMessage(newReposJSON) + } + } + + data, err := json.MarshalIndent(policyContentStruct, "", " ") + if err != nil { + return fmt.Errorf("error setting trust policy: %w", err) + } + return ioutil.WriteFile(policyPath, data, 0644) +} diff --git a/pkg/trust/policy_test.go b/pkg/trust/policy_test.go new file mode 100644 index 000000000..3952b72c3 --- /dev/null +++ b/pkg/trust/policy_test.go @@ -0,0 +1,196 @@ +package trust + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/containers/image/v5/signature" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAddPolicyEntries(t *testing.T) { + tempDir := t.TempDir() + policyPath := filepath.Join(tempDir, "policy.json") + + minimalPolicy := &signature.Policy{ + Default: []signature.PolicyRequirement{ + signature.NewPRInsecureAcceptAnything(), + }, + } + minimalPolicyJSON, err := json.Marshal(minimalPolicy) + require.NoError(t, err) + err = os.WriteFile(policyPath, minimalPolicyJSON, 0600) + require.NoError(t, err) + + // Invalid input: + for _, invalid := range []AddPolicyEntriesInput{ + { + Scope: "default", + Type: "accept", + PubKeyFiles: []string{"/does-not-make-sense"}, + }, + { + Scope: "default", + Type: "insecureAcceptAnything", + PubKeyFiles: []string{"/does-not-make-sense"}, + }, + { + Scope: "default", + Type: "reject", + PubKeyFiles: []string{"/does-not-make-sense"}, + }, + { + Scope: "default", + Type: "signedBy", + PubKeyFiles: []string{}, // A key is missing + }, + { + Scope: "default", + Type: "sigstoreSigned", + PubKeyFiles: []string{}, // A key is missing + }, + { + Scope: "default", + Type: "this-is-unknown", + PubKeyFiles: []string{}, + }, + } { + err := AddPolicyEntries(policyPath, invalid) + assert.Error(t, err, "%#v", invalid) + } + + err = AddPolicyEntries(policyPath, AddPolicyEntriesInput{ + Scope: "default", + Type: "reject", + }) + assert.NoError(t, err) + err = AddPolicyEntries(policyPath, AddPolicyEntriesInput{ + Scope: "quay.io/accepted", + Type: "accept", + }) + assert.NoError(t, err) + err = AddPolicyEntries(policyPath, AddPolicyEntriesInput{ + Scope: "quay.io/multi-signed", + Type: "signedBy", + PubKeyFiles: []string{"/1.pub", "/2.pub"}, + }) + assert.NoError(t, err) + err = AddPolicyEntries(policyPath, AddPolicyEntriesInput{ + Scope: "quay.io/sigstore-signed", + Type: "sigstoreSigned", + PubKeyFiles: []string{"/1.pub", "/2.pub"}, + }) + assert.NoError(t, err) + + // Test that the outcome is consumable, and compare it with the expected values. + parsedPolicy, err := signature.NewPolicyFromFile(policyPath) + require.NoError(t, err) + assert.Equal(t, &signature.Policy{ + Default: signature.PolicyRequirements{ + signature.NewPRReject(), + }, + Transports: map[string]signature.PolicyTransportScopes{ + "docker": { + "quay.io/accepted": { + signature.NewPRInsecureAcceptAnything(), + }, + "quay.io/multi-signed": { + xNewPRSignedByKeyPath(t, "/1.pub", signature.NewPRMMatchRepoDigestOrExact()), + xNewPRSignedByKeyPath(t, "/2.pub", signature.NewPRMMatchRepoDigestOrExact()), + }, + "quay.io/sigstore-signed": { + xNewPRSigstoreSignedKeyPath(t, "/1.pub", signature.NewPRMMatchRepoDigestOrExact()), + xNewPRSigstoreSignedKeyPath(t, "/2.pub", signature.NewPRMMatchRepoDigestOrExact()), + }, + }, + }, + }, parsedPolicy) + + // Test that completely unknown JSON is preserved + jsonWithUnknownData := `{ + "default": [ + { + "type": "this is unknown", + "unknown field": "should be preserved" + } + ], + "transports": + { + "docker-daemon": + { + "": [{ + "type":"this is unknown 2", + "unknown field 2": "should be preserved 2" + }] + } + } +}` + err = os.WriteFile(policyPath, []byte(jsonWithUnknownData), 0600) + require.NoError(t, err) + err = AddPolicyEntries(policyPath, AddPolicyEntriesInput{ + Scope: "quay.io/innocuous", + Type: "signedBy", + PubKeyFiles: []string{"/1.pub"}, + }) + require.NoError(t, err) + updatedJSONWithUnknownData, err := os.ReadFile(policyPath) + require.NoError(t, err) + // Decode updatedJSONWithUnknownData so that this test does not depend on details of the encoding. + // To reduce noise in the constants below: + type a = []interface{} + type m = map[string]interface{} + var parsedUpdatedJSON m + err = json.Unmarshal(updatedJSONWithUnknownData, &parsedUpdatedJSON) + require.NoError(t, err) + assert.Equal(t, m{ + "default": a{ + m{ + "type": "this is unknown", + "unknown field": "should be preserved", + }, + }, + "transports": m{ + "docker-daemon": m{ + "": a{ + m{ + "type": "this is unknown 2", + "unknown field 2": "should be preserved 2", + }, + }, + }, + "docker": m{ + "quay.io/innocuous": a{ + m{ + "type": "signedBy", + "keyType": "GPGKeys", + "keyPath": "/1.pub", + }, + }, + }, + }, + }, parsedUpdatedJSON) +} + +// xNewPRSignedByKeyPath is a wrapper for NewPRSignedByKeyPath which must not fail. +func xNewPRSignedByKeyPath(t *testing.T, keyPath string, signedIdentity signature.PolicyReferenceMatch) signature.PolicyRequirement { + pr, err := signature.NewPRSignedByKeyPath(signature.SBKeyTypeGPGKeys, keyPath, signedIdentity) + require.NoError(t, err) + return pr +} + +// xNewPRSignedByKeyPaths is a wrapper for NewPRSignedByKeyPaths which must not fail. +func xNewPRSignedByKeyPaths(t *testing.T, keyPaths []string, signedIdentity signature.PolicyReferenceMatch) signature.PolicyRequirement { + pr, err := signature.NewPRSignedByKeyPaths(signature.SBKeyTypeGPGKeys, keyPaths, signedIdentity) + require.NoError(t, err) + return pr +} + +// xNewPRSigstoreSignedKeyPath is a wrapper for NewPRSigstoreSignedKeyPath which must not fail. +func xNewPRSigstoreSignedKeyPath(t *testing.T, keyPath string, signedIdentity signature.PolicyReferenceMatch) signature.PolicyRequirement { + pr, err := signature.NewPRSigstoreSignedKeyPath(keyPath, signedIdentity) + require.NoError(t, err) + return pr +} diff --git a/pkg/trust/registries.go b/pkg/trust/registries.go new file mode 100644 index 000000000..0adc38232 --- /dev/null +++ b/pkg/trust/registries.go @@ -0,0 +1,126 @@ +package trust + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/containers/image/v5/types" + "github.com/docker/docker/pkg/homedir" + "github.com/ghodss/yaml" +) + +// registryConfiguration is one of the files in registriesDirPath configuring lookaside locations, or the result of merging them all. +// NOTE: Keep this in sync with docs/registries.d.md! +type registryConfiguration struct { + DefaultDocker *registryNamespace `json:"default-docker"` + // The key is a namespace, using fully-expanded Docker reference format or parent namespaces (per dockerReference.PolicyConfiguration*), + Docker map[string]registryNamespace `json:"docker"` +} + +// registryNamespace defines lookaside locations for a single namespace. +type registryNamespace struct { + Lookaside string `json:"lookaside"` // For reading, and if LookasideStaging is not present, for writing. + LookasideStaging string `json:"lookaside-staging"` // For writing only. + SigStore string `json:"sigstore"` // For reading, and if SigStoreStaging is not present, for writing. + SigStoreStaging string `json:"sigstore-staging"` // For writing only. +} + +// systemRegistriesDirPath is the path to registries.d. +const systemRegistriesDirPath = "/etc/containers/registries.d" + +// userRegistriesDir is the path to the per user registries.d. +var userRegistriesDir = filepath.FromSlash(".config/containers/registries.d") + +// RegistriesDirPath returns a path to registries.d +func RegistriesDirPath(sys *types.SystemContext) string { + if sys != nil && sys.RegistriesDirPath != "" { + return sys.RegistriesDirPath + } + userRegistriesDirPath := filepath.Join(homedir.Get(), userRegistriesDir) + if _, err := os.Stat(userRegistriesDirPath); err == nil { + return userRegistriesDirPath + } + if sys != nil && sys.RootForImplicitAbsolutePaths != "" { + return filepath.Join(sys.RootForImplicitAbsolutePaths, systemRegistriesDirPath) + } + + return systemRegistriesDirPath +} + +// loadAndMergeConfig loads registries.d configuration files in dirPath +func loadAndMergeConfig(dirPath string) (*registryConfiguration, error) { + mergedConfig := registryConfiguration{Docker: map[string]registryNamespace{}} + dockerDefaultMergedFrom := "" + nsMergedFrom := map[string]string{} + + dir, err := os.Open(dirPath) + if err != nil { + if os.IsNotExist(err) { + return &mergedConfig, nil + } + return nil, err + } + configNames, err := dir.Readdirnames(0) + if err != nil { + return nil, err + } + for _, configName := range configNames { + if !strings.HasSuffix(configName, ".yaml") { + continue + } + configPath := filepath.Join(dirPath, configName) + configBytes, err := ioutil.ReadFile(configPath) + if err != nil { + return nil, err + } + var config registryConfiguration + err = yaml.Unmarshal(configBytes, &config) + if err != nil { + return nil, fmt.Errorf("error parsing %s: %w", configPath, err) + } + if config.DefaultDocker != nil { + if mergedConfig.DefaultDocker != nil { + return nil, fmt.Errorf(`error parsing signature storage configuration: "default-docker" defined both in "%s" and "%s"`, + dockerDefaultMergedFrom, configPath) + } + mergedConfig.DefaultDocker = config.DefaultDocker + dockerDefaultMergedFrom = configPath + } + for nsName, nsConfig := range config.Docker { // includes config.Docker == nil + if _, ok := mergedConfig.Docker[nsName]; ok { + return nil, fmt.Errorf(`error parsing signature storage configuration: "docker" namespace "%s" defined both in "%s" and "%s"`, + nsName, nsMergedFrom[nsName], configPath) + } + mergedConfig.Docker[nsName] = nsConfig + nsMergedFrom[nsName] = configPath + } + } + return &mergedConfig, nil +} + +// registriesDConfigurationForScope returns registries.d configuration for the provided scope. +// scope can be "" to return only the global default configuration entry. +func registriesDConfigurationForScope(registryConfigs *registryConfiguration, scope string) *registryNamespace { + searchScope := scope + if searchScope != "" { + if !strings.Contains(searchScope, "/") { + val, exists := registryConfigs.Docker[searchScope] + if exists { + return &val + } + } + for range strings.Split(scope, "/") { + val, exists := registryConfigs.Docker[searchScope] + if exists { + return &val + } + if strings.Contains(searchScope, "/") { + searchScope = searchScope[:strings.LastIndex(searchScope, "/")] + } + } + } + return registryConfigs.DefaultDocker +} diff --git a/pkg/trust/testdata/default.yaml b/pkg/trust/testdata/default.yaml new file mode 100644 index 000000000..31bcd35ef --- /dev/null +++ b/pkg/trust/testdata/default.yaml @@ -0,0 +1,25 @@ +# This is a default registries.d configuration file. You may +# add to this file or create additional files in registries.d/. +# +# lookaside: indicates a location that is read and write +# lookaside-staging: indicates a location that is only for write +# +# lookaside and lookaside-staging take a value of the following: +# lookaside: {schema}://location +# +# For reading signatures, schema may be http, https, or file. +# For writing signatures, schema may only be file. + +# This is the default signature write location for docker registries. +default-docker: +# lookaside: file:///var/lib/containers/sigstore + lookaside-staging: file:///var/lib/containers/sigstore + +# The 'docker' indicator here is the start of the configuration +# for docker registries. +# +# docker: +# +# privateregistry.com: +# lookaside: http://privateregistry.com/sigstore/ +# lookaside-staging: /mnt/nfs/privateregistry/sigstore diff --git a/pkg/trust/testdata/quay.io.yaml b/pkg/trust/testdata/quay.io.yaml new file mode 100644 index 000000000..80071596d --- /dev/null +++ b/pkg/trust/testdata/quay.io.yaml @@ -0,0 +1,3 @@ +docker: + quay.io/multi-signed: + lookaside: https://quay.example.com/sigstore diff --git a/pkg/trust/testdata/redhat.yaml b/pkg/trust/testdata/redhat.yaml new file mode 100644 index 000000000..8e40a4174 --- /dev/null +++ b/pkg/trust/testdata/redhat.yaml @@ -0,0 +1,5 @@ +docker: + registry.redhat.io: + sigstore: https://registry.redhat.io/containers/sigstore + registry.access.redhat.com: + sigstore: https://registry.redhat.io/containers/sigstore diff --git a/pkg/trust/trust.go b/pkg/trust/trust.go index 663a1b5e2..07d144bc1 100644 --- a/pkg/trust/trust.go +++ b/pkg/trust/trust.go @@ -1,243 +1,127 @@ package trust import ( - "bufio" - "bytes" - "encoding/base64" - "encoding/json" "fmt" - "io/ioutil" - "os" - "os/exec" - "path/filepath" + "sort" "strings" - - "github.com/containers/image/v5/types" - "github.com/docker/docker/pkg/homedir" - "github.com/ghodss/yaml" - "github.com/sirupsen/logrus" ) -// PolicyContent struct for policy.json file -type PolicyContent struct { - Default []RepoContent `json:"default"` - Transports TransportsContent `json:"transports,omitempty"` -} - -// RepoContent struct used under each repo -type RepoContent struct { - Type string `json:"type"` - KeyType string `json:"keyType,omitempty"` - KeyPath string `json:"keyPath,omitempty"` - KeyData string `json:"keyData,omitempty"` - SignedIdentity json.RawMessage `json:"signedIdentity,omitempty"` -} - -// RepoMap map repo name to policycontent for each repo -type RepoMap map[string][]RepoContent - -// TransportsContent struct for content under "transports" -type TransportsContent map[string]RepoMap - -// RegistryConfiguration is one of the files in registriesDirPath configuring lookaside locations, or the result of merging them all. -// NOTE: Keep this in sync with docs/registries.d.md! -type RegistryConfiguration struct { - DefaultDocker *RegistryNamespace `json:"default-docker"` - // The key is a namespace, using fully-expanded Docker reference format or parent namespaces (per dockerReference.PolicyConfiguration*), - Docker map[string]RegistryNamespace `json:"docker"` +// Policy describes a basic trust policy configuration +type Policy struct { + Transport string `json:"transport"` + Name string `json:"name,omitempty"` + RepoName string `json:"repo_name,omitempty"` + Keys []string `json:"keys,omitempty"` + SignatureStore string `json:"sigstore,omitempty"` + Type string `json:"type"` + GPGId string `json:"gpg_id,omitempty"` } -// RegistryNamespace defines lookaside locations for a single namespace. -type RegistryNamespace struct { - SigStore string `json:"sigstore"` // For reading, and if SigStoreStaging is not present, for writing. - SigStoreStaging string `json:"sigstore-staging"` // For writing only. +// PolicyDescription returns an user-focused description of the policy in policyPath and registries.d data from registriesDirPath. +func PolicyDescription(policyPath, registriesDirPath string) ([]*Policy, error) { + return policyDescriptionWithGPGIDReader(policyPath, registriesDirPath, getGPGIdFromKeyPath) } -// ShowOutput keep the fields for image trust show command -type ShowOutput struct { - Repo string - Trusttype string - GPGid string - Sigstore string -} - -// systemRegistriesDirPath is the path to registries.d. -const systemRegistriesDirPath = "/etc/containers/registries.d" - -// userRegistriesDir is the path to the per user registries.d. -var userRegistriesDir = filepath.FromSlash(".config/containers/registries.d") - -// DefaultPolicyPath returns a path to the default policy of the system. -func DefaultPolicyPath(sys *types.SystemContext) string { - systemDefaultPolicyPath := "/etc/containers/policy.json" - if sys != nil { - if sys.SignaturePolicyPath != "" { - return sys.SignaturePolicyPath - } - if sys.RootForImplicitAbsolutePaths != "" { - return filepath.Join(sys.RootForImplicitAbsolutePaths, systemDefaultPolicyPath) - } - } - return systemDefaultPolicyPath -} - -// RegistriesDirPath returns a path to registries.d -func RegistriesDirPath(sys *types.SystemContext) string { - if sys != nil && sys.RegistriesDirPath != "" { - return sys.RegistriesDirPath - } - userRegistriesDirPath := filepath.Join(homedir.Get(), userRegistriesDir) - if _, err := os.Stat(userRegistriesDirPath); err == nil { - return userRegistriesDirPath +// policyDescriptionWithGPGIDReader is PolicyDescription with a gpgIDReader parameter. It exists only to make testing easier. +func policyDescriptionWithGPGIDReader(policyPath, registriesDirPath string, idReader gpgIDReader) ([]*Policy, error) { + policyContentStruct, err := getPolicy(policyPath) + if err != nil { + return nil, fmt.Errorf("could not read trust policies: %w", err) } - if sys != nil && sys.RootForImplicitAbsolutePaths != "" { - return filepath.Join(sys.RootForImplicitAbsolutePaths, systemRegistriesDirPath) + res, err := getPolicyShowOutput(policyContentStruct, registriesDirPath, idReader) + if err != nil { + return nil, fmt.Errorf("could not show trust policies: %w", err) } - - return systemRegistriesDirPath + return res, nil } -// LoadAndMergeConfig loads configuration files in dirPath -func LoadAndMergeConfig(dirPath string) (*RegistryConfiguration, error) { - mergedConfig := RegistryConfiguration{Docker: map[string]RegistryNamespace{}} - dockerDefaultMergedFrom := "" - nsMergedFrom := map[string]string{} +func getPolicyShowOutput(policyContentStruct policyContent, systemRegistriesDirPath string, idReader gpgIDReader) ([]*Policy, error) { + var output []*Policy - dir, err := os.Open(dirPath) + registryConfigs, err := loadAndMergeConfig(systemRegistriesDirPath) if err != nil { - if os.IsNotExist(err) { - return &mergedConfig, nil - } return nil, err } - configNames, err := dir.Readdirnames(0) - if err != nil { - return nil, err - } - for _, configName := range configNames { - if !strings.HasSuffix(configName, ".yaml") { - continue - } - configPath := filepath.Join(dirPath, configName) - configBytes, err := ioutil.ReadFile(configPath) - if err != nil { - return nil, err + + if len(policyContentStruct.Default) > 0 { + template := Policy{ + Transport: "all", + Name: "* (default)", + RepoName: "default", } - var config RegistryConfiguration - err = yaml.Unmarshal(configBytes, &config) - if err != nil { - return nil, fmt.Errorf("error parsing %s: %w", configPath, err) + output = append(output, descriptionsOfPolicyRequirements(policyContentStruct.Default, template, registryConfigs, "", idReader)...) + } + // FIXME: This should use x/exp/maps.Keys after we update to Go 1.18. + transports := []string{} + for t := range policyContentStruct.Transports { + transports = append(transports, t) + } + sort.Strings(transports) + for _, transport := range transports { + transval := policyContentStruct.Transports[transport] + if transport == "docker" { + transport = "repository" } - if config.DefaultDocker != nil { - if mergedConfig.DefaultDocker != nil { - return nil, fmt.Errorf(`error parsing signature storage configuration: "default-docker" defined both in "%s" and "%s"`, - dockerDefaultMergedFrom, configPath) - } - mergedConfig.DefaultDocker = config.DefaultDocker - dockerDefaultMergedFrom = configPath + + // FIXME: This should use x/exp/maps.Keys after we update to Go 1.18. + scopes := []string{} + for s := range transval { + scopes = append(scopes, s) } - for nsName, nsConfig := range config.Docker { // includes config.Docker == nil - if _, ok := mergedConfig.Docker[nsName]; ok { - return nil, fmt.Errorf(`error parsing signature storage configuration: "docker" namespace "%s" defined both in "%s" and "%s"`, - nsName, nsMergedFrom[nsName], configPath) + sort.Strings(scopes) + for _, repo := range scopes { + repoval := transval[repo] + template := Policy{ + Transport: transport, + Name: repo, + RepoName: repo, } - mergedConfig.Docker[nsName] = nsConfig - nsMergedFrom[nsName] = configPath + output = append(output, descriptionsOfPolicyRequirements(repoval, template, registryConfigs, repo, idReader)...) } } - return &mergedConfig, nil + return output, nil } -// HaveMatchRegistry checks if trust settings for the registry have been configured in yaml file -func HaveMatchRegistry(key string, registryConfigs *RegistryConfiguration) *RegistryNamespace { - searchKey := key - if !strings.Contains(searchKey, "/") { - val, exists := registryConfigs.Docker[searchKey] - if exists { - return &val - } - } - for range strings.Split(key, "/") { - val, exists := registryConfigs.Docker[searchKey] - if exists { - return &val - } - if strings.Contains(searchKey, "/") { - searchKey = searchKey[:strings.LastIndex(searchKey, "/")] +// descriptionsOfPolicyRequirements turns reqs into user-readable policy entries, with Transport/Name/Reponame coming from template, potentially looking up scope (which may be "") in registryConfigs. +func descriptionsOfPolicyRequirements(reqs []repoContent, template Policy, registryConfigs *registryConfiguration, scope string, idReader gpgIDReader) []*Policy { + res := []*Policy{} + + var lookasidePath string + registryNamespace := registriesDConfigurationForScope(registryConfigs, scope) + if registryNamespace != nil { + if registryNamespace.Lookaside != "" { + lookasidePath = registryNamespace.Lookaside + } else { // incl. registryNamespace.SigStore == "" + lookasidePath = registryNamespace.SigStore } } - return registryConfigs.DefaultDocker -} - -// CreateTmpFile creates a temp file under dir and writes the content into it -func CreateTmpFile(dir, pattern string, content []byte) (string, error) { - tmpfile, err := ioutil.TempFile(dir, pattern) - if err != nil { - return "", err - } - defer tmpfile.Close() - - if _, err := tmpfile.Write(content); err != nil { - return "", err - } - return tmpfile.Name(), nil -} - -// GetGPGIdFromKeyPath return user keyring from key path -func GetGPGIdFromKeyPath(path string) []string { - cmd := exec.Command("gpg2", "--with-colons", path) - results, err := cmd.Output() - if err != nil { - logrus.Errorf("Getting key identity: %s", err) - return nil - } - return parseUids(results) -} -// GetGPGIdFromKeyData return user keyring from keydata -func GetGPGIdFromKeyData(key string) []string { - decodeKey, err := base64.StdEncoding.DecodeString(key) - if err != nil { - logrus.Errorf("%s, error decoding key data", err) - return nil - } - tmpfileName, err := CreateTmpFile("", "", decodeKey) - if err != nil { - logrus.Errorf("Creating key date temp file %s", err) - } - defer os.Remove(tmpfileName) - return GetGPGIdFromKeyPath(tmpfileName) -} + for _, repoele := range reqs { + entry := template + entry.Type = trustTypeDescription(repoele.Type) -func parseUids(colonDelimitKeys []byte) []string { - var parseduids []string - scanner := bufio.NewScanner(bytes.NewReader(colonDelimitKeys)) - for scanner.Scan() { - line := scanner.Text() - if strings.HasPrefix(line, "uid:") || strings.HasPrefix(line, "pub:") { - uid := strings.Split(line, ":")[9] - if uid == "" { - continue + var gpgIDString string + switch repoele.Type { + case "signedBy": + uids := []string{} + if len(repoele.KeyPath) > 0 { + uids = append(uids, idReader(repoele.KeyPath)...) } - parseduid := uid - if strings.Contains(uid, "<") && strings.Contains(uid, ">") { - parseduid = strings.SplitN(strings.SplitAfterN(uid, "<", 2)[1], ">", 2)[0] + for _, path := range repoele.KeyPaths { + uids = append(uids, idReader(path)...) } - parseduids = append(parseduids, parseduid) + if len(repoele.KeyData) > 0 { + uids = append(uids, getGPGIdFromKeyData(idReader, repoele.KeyData)...) + } + gpgIDString = strings.Join(uids, ", ") + + case "sigstoreSigned": + gpgIDString = "N/A" // We could potentially return key fingerprints here, but they would not be _GPG_ fingerprints. } + entry.GPGId = gpgIDString + entry.SignatureStore = lookasidePath // We do this even for sigstoreSigned and things like type: reject, to show that the sigstore is being read. + res = append(res, &entry) } - return parseduids -} -// GetPolicy parse policy.json into PolicyContent struct -func GetPolicy(policyPath string) (PolicyContent, error) { - var policyContentStruct PolicyContent - policyContent, err := ioutil.ReadFile(policyPath) - if err != nil { - return policyContentStruct, fmt.Errorf("unable to read policy file: %w", err) - } - if err := json.Unmarshal(policyContent, &policyContentStruct); err != nil { - return policyContentStruct, fmt.Errorf("could not parse trust policies from %s: %w", policyPath, err) - } - return policyContentStruct, nil + return res } diff --git a/pkg/trust/trust_test.go b/pkg/trust/trust_test.go new file mode 100644 index 000000000..22b780fc9 --- /dev/null +++ b/pkg/trust/trust_test.go @@ -0,0 +1,376 @@ +package trust + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/containers/image/v5/signature" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPolicyDescription(t *testing.T) { + tempDir := t.TempDir() + policyPath := filepath.Join(tempDir, "policy.json") + + // Override getGPGIdFromKeyPath because we don't want to bother with (and spend the unit-test time on) generating valid GPG keys, and running the real GPG binary. + // Instead of reading the files at all, just expect file names like /id1,id2,...,idN.pub + idReader := func(keyPath string) []string { + require.True(t, strings.HasPrefix(keyPath, "/")) + require.True(t, strings.HasSuffix(keyPath, ".pub")) + return strings.Split(keyPath[1:len(keyPath)-4], ",") + } + + for _, c := range []struct { + policy *signature.Policy + expected []*Policy + }{ + { + &signature.Policy{ + Default: signature.PolicyRequirements{ + signature.NewPRReject(), + }, + Transports: map[string]signature.PolicyTransportScopes{ + "docker": { + "quay.io/accepted": { + signature.NewPRInsecureAcceptAnything(), + }, + "registry.redhat.io": { + xNewPRSignedByKeyPath(t, "/redhat.pub", signature.NewPRMMatchRepoDigestOrExact()), + }, + "registry.access.redhat.com": { + xNewPRSignedByKeyPaths(t, []string{"/redhat.pub", "/redhat-beta.pub"}, signature.NewPRMMatchRepoDigestOrExact()), + }, + "quay.io/multi-signed": { + xNewPRSignedByKeyPath(t, "/1.pub", signature.NewPRMMatchRepoDigestOrExact()), + xNewPRSignedByKeyPath(t, "/2,3.pub", signature.NewPRMMatchRepoDigestOrExact()), + }, + "quay.io/sigstore-signed": { + xNewPRSigstoreSignedKeyPath(t, "/1.pub", signature.NewPRMMatchRepoDigestOrExact()), + xNewPRSigstoreSignedKeyPath(t, "/2.pub", signature.NewPRMMatchRepoDigestOrExact()), + }, + }, + }, + }, + []*Policy{ + { + Transport: "all", + Name: "* (default)", + RepoName: "default", + Type: "reject", + }, + { + Transport: "repository", + Name: "quay.io/accepted", + RepoName: "quay.io/accepted", + Type: "accept", + }, + { + Transport: "repository", + Name: "quay.io/multi-signed", + RepoName: "quay.io/multi-signed", + Type: "signed", + SignatureStore: "https://quay.example.com/sigstore", + GPGId: "1", + }, + { + Transport: "repository", + Name: "quay.io/multi-signed", + RepoName: "quay.io/multi-signed", + Type: "signed", + SignatureStore: "https://quay.example.com/sigstore", + GPGId: "2, 3", + }, + { + Transport: "repository", + Name: "quay.io/sigstore-signed", + RepoName: "quay.io/sigstore-signed", + Type: "sigstoreSigned", + SignatureStore: "", + GPGId: "N/A", + }, + { + Transport: "repository", + Name: "quay.io/sigstore-signed", + RepoName: "quay.io/sigstore-signed", + Type: "sigstoreSigned", + SignatureStore: "", + GPGId: "N/A", + }, + { + Transport: "repository", + Name: "registry.access.redhat.com", + RepoName: "registry.access.redhat.com", + Type: "signed", + SignatureStore: "https://registry.redhat.io/containers/sigstore", + GPGId: "redhat, redhat-beta", + }, { + Transport: "repository", + Name: "registry.redhat.io", + RepoName: "registry.redhat.io", + Type: "signed", + SignatureStore: "https://registry.redhat.io/containers/sigstore", + GPGId: "redhat", + }, + }, + }, + { + &signature.Policy{ + Default: signature.PolicyRequirements{ + xNewPRSignedByKeyPath(t, "/1.pub", signature.NewPRMMatchRepoDigestOrExact()), + xNewPRSignedByKeyPath(t, "/2,3.pub", signature.NewPRMMatchRepoDigestOrExact()), + }, + }, + []*Policy{ + { + Transport: "all", + Name: "* (default)", + RepoName: "default", + Type: "signed", + SignatureStore: "", + GPGId: "1", + }, + { + Transport: "all", + Name: "* (default)", + RepoName: "default", + Type: "signed", + SignatureStore: "", + GPGId: "2, 3", + }, + }, + }, + } { + policyJSON, err := json.Marshal(c.policy) + require.NoError(t, err) + err = os.WriteFile(policyPath, policyJSON, 0600) + require.NoError(t, err) + + res, err := policyDescriptionWithGPGIDReader(policyPath, "./testdata", idReader) + require.NoError(t, err) + assert.Equal(t, c.expected, res) + } +} + +func TestDescriptionsOfPolicyRequirements(t *testing.T) { + // Override getGPGIdFromKeyPath because we don't want to bother with (and spend the unit-test time on) generating valid GPG keys, and running the real GPG binary. + // Instead of reading the files at all, just expect file names like /id1,id2,...,idN.pub + idReader := func(keyPath string) []string { + require.True(t, strings.HasPrefix(keyPath, "/")) + require.True(t, strings.HasSuffix(keyPath, ".pub")) + return strings.Split(keyPath[1:len(keyPath)-4], ",") + } + + template := Policy{ + Transport: "transport", + Name: "name", + RepoName: "repoName", + } + registryConfigs, err := loadAndMergeConfig("./testdata") + require.NoError(t, err) + + for _, c := range []struct { + scope string + reqs signature.PolicyRequirements + expected []*Policy + }{ + { + "", + signature.PolicyRequirements{ + signature.NewPRReject(), + }, + []*Policy{ + { + Transport: "transport", + Name: "name", + RepoName: "repoName", + Type: "reject", + }, + }, + }, + { + "quay.io/accepted", + signature.PolicyRequirements{ + signature.NewPRInsecureAcceptAnything(), + }, + []*Policy{ + { + Transport: "transport", + Name: "name", + RepoName: "repoName", + Type: "accept", + }, + }, + }, + { + "registry.redhat.io", + signature.PolicyRequirements{ + xNewPRSignedByKeyPath(t, "/redhat.pub", signature.NewPRMMatchRepoDigestOrExact()), + }, + []*Policy{ + { + Transport: "transport", + Name: "name", + RepoName: "repoName", + Type: "signed", + SignatureStore: "https://registry.redhat.io/containers/sigstore", + GPGId: "redhat", + }, + }, + }, + { + "registry.access.redhat.com", + signature.PolicyRequirements{ + xNewPRSignedByKeyPaths(t, []string{"/redhat.pub", "/redhat-beta.pub"}, signature.NewPRMMatchRepoDigestOrExact()), + }, + []*Policy{ + { + Transport: "transport", + Name: "name", + RepoName: "repoName", + Type: "signed", + SignatureStore: "https://registry.redhat.io/containers/sigstore", + GPGId: "redhat, redhat-beta", + }, + }, + }, + { + "quay.io/multi-signed", + signature.PolicyRequirements{ + xNewPRSignedByKeyPath(t, "/1.pub", signature.NewPRMMatchRepoDigestOrExact()), + xNewPRSignedByKeyPath(t, "/2,3.pub", signature.NewPRMMatchRepoDigestOrExact()), + }, + []*Policy{ + { + Transport: "transport", + Name: "name", + RepoName: "repoName", + Type: "signed", + SignatureStore: "https://quay.example.com/sigstore", + GPGId: "1", + }, + { + Transport: "transport", + Name: "name", + RepoName: "repoName", + Type: "signed", + SignatureStore: "https://quay.example.com/sigstore", + GPGId: "2, 3", + }, + }, + }, { + "quay.io/sigstore-signed", + signature.PolicyRequirements{ + xNewPRSigstoreSignedKeyPath(t, "/1.pub", signature.NewPRMMatchRepoDigestOrExact()), + xNewPRSigstoreSignedKeyPath(t, "/2.pub", signature.NewPRMMatchRepoDigestOrExact()), + }, + []*Policy{ + { + Transport: "transport", + Name: "name", + RepoName: "repoName", + Type: "sigstoreSigned", + SignatureStore: "", + GPGId: "N/A", + }, + { + Transport: "transport", + Name: "name", + RepoName: "repoName", + Type: "sigstoreSigned", + SignatureStore: "", + GPGId: "N/A", + }, + }, + }, + { // Multiple kinds of requirements are represented individually. + "registry.redhat.io", + signature.PolicyRequirements{ + signature.NewPRReject(), + signature.NewPRInsecureAcceptAnything(), + xNewPRSignedByKeyPath(t, "/redhat.pub", signature.NewPRMMatchRepoDigestOrExact()), + xNewPRSignedByKeyPaths(t, []string{"/redhat.pub", "/redhat-beta.pub"}, signature.NewPRMMatchRepoDigestOrExact()), + xNewPRSignedByKeyPath(t, "/1.pub", signature.NewPRMMatchRepoDigestOrExact()), + xNewPRSignedByKeyPath(t, "/2,3.pub", signature.NewPRMMatchRepoDigestOrExact()), + xNewPRSigstoreSignedKeyPath(t, "/1.pub", signature.NewPRMMatchRepoDigestOrExact()), + xNewPRSigstoreSignedKeyPath(t, "/2.pub", signature.NewPRMMatchRepoDigestOrExact()), + }, + []*Policy{ + { + Transport: "transport", + Name: "name", + RepoName: "repoName", + SignatureStore: "https://registry.redhat.io/containers/sigstore", + Type: "reject", + }, + { + Transport: "transport", + Name: "name", + RepoName: "repoName", + SignatureStore: "https://registry.redhat.io/containers/sigstore", + Type: "accept", + }, + { + Transport: "transport", + Name: "name", + RepoName: "repoName", + Type: "signed", + SignatureStore: "https://registry.redhat.io/containers/sigstore", + GPGId: "redhat", + }, + { + Transport: "transport", + Name: "name", + RepoName: "repoName", + Type: "signed", + SignatureStore: "https://registry.redhat.io/containers/sigstore", + GPGId: "redhat, redhat-beta", + }, + { + Transport: "transport", + Name: "name", + RepoName: "repoName", + Type: "signed", + SignatureStore: "https://registry.redhat.io/containers/sigstore", + GPGId: "1", + }, + { + Transport: "transport", + Name: "name", + RepoName: "repoName", + Type: "signed", + SignatureStore: "https://registry.redhat.io/containers/sigstore", + GPGId: "2, 3", + }, + { + Transport: "transport", + Name: "name", + RepoName: "repoName", + Type: "sigstoreSigned", + SignatureStore: "https://registry.redhat.io/containers/sigstore", + GPGId: "N/A", + }, + { + Transport: "transport", + Name: "name", + RepoName: "repoName", + Type: "sigstoreSigned", + SignatureStore: "https://registry.redhat.io/containers/sigstore", + GPGId: "N/A", + }, + }, + }, + } { + reqsJSON, err := json.Marshal(c.reqs) + require.NoError(t, err) + var parsedRegs []repoContent + err = json.Unmarshal(reqsJSON, &parsedRegs) + require.NoError(t, err) + + res := descriptionsOfPolicyRequirements(parsedRegs, template, registryConfigs, c.scope, idReader) + assert.Equal(t, c.expected, res) + } +} |