Skip to content

Commit 3ed43d3

Browse files
committed
add logic to check container policy to block operator bundle projects
Signed-off-by: Adam D. Cornett <adc@redhat.com>
1 parent d59f70a commit 3ed43d3

File tree

7 files changed

+169
-105
lines changed

7 files changed

+169
-105
lines changed

container/check_container.go

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@ package container
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"net/http"
78
goruntime "runtime"
89
"time"
910

11+
"github.com/go-logr/logr"
12+
1013
"github.com/redhat-openshift-ecosystem/openshift-preflight/certification"
1114
preflighterr "github.com/redhat-openshift-ecosystem/openshift-preflight/errors"
1215
"github.com/redhat-openshift-ecosystem/openshift-preflight/internal/check"
1316
"github.com/redhat-openshift-ecosystem/openshift-preflight/internal/engine"
1417
"github.com/redhat-openshift-ecosystem/openshift-preflight/internal/lib"
18+
"github.com/redhat-openshift-ecosystem/openshift-preflight/internal/log"
1519
"github.com/redhat-openshift-ecosystem/openshift-preflight/internal/policy"
1620
"github.com/redhat-openshift-ecosystem/openshift-preflight/internal/pyxis"
1721
"github.com/redhat-openshift-ecosystem/openshift-preflight/internal/runtime"
@@ -65,6 +69,7 @@ func (c *containerCheck) Run(ctx context.Context) (certification.Results, error)
6569
}
6670

6771
func (c *containerCheck) resolve(ctx context.Context) error {
72+
logger := logr.FromContextOrDiscard(ctx)
6873
if c.resolved {
6974
return nil
7075
}
@@ -76,18 +81,31 @@ func (c *containerCheck) resolve(ctx context.Context) error {
7681
c.policy = policy.PolicyContainer
7782

7883
// If we have enough Pyxis information, resolve the policy.
79-
if c.hasPyxisData() {
80-
p := pyxis.NewPyxisClient(
81-
c.pyxisHost,
82-
c.pyxisToken,
83-
c.certificationProjectID,
84-
&http.Client{Timeout: 60 * time.Second},
85-
)
86-
87-
override, err := lib.GetContainerPolicyExceptions(ctx, p)
84+
if c.pyxisClient != nil || c.hasPyxisData() {
85+
// Use injected client for testing, otherwise create a new one
86+
p := c.pyxisClient
87+
if p == nil {
88+
p = pyxis.NewPyxisClient(
89+
c.pyxisHost,
90+
c.pyxisToken,
91+
c.certificationProjectID,
92+
&http.Client{Timeout: 60 * time.Second},
93+
)
94+
}
95+
96+
certProject, err := p.GetProject(ctx)
8897
if err != nil {
89-
return fmt.Errorf("%w: %s", preflighterr.ErrCannotResolvePolicyException, err)
98+
return fmt.Errorf("%w: could not retrieve project: %s", preflighterr.ErrCannotResolvePolicyException, err)
9099
}
100+
logger.V(log.DBG).Info("certification project", "name", certProject.Name)
101+
102+
// checking to see if project is an operator bundle, to safeguard against users submitting containers to bundle projects
103+
if certProject.BundleProject() {
104+
return errors.New("bundle project detected: container submissions are not valid for bundle projects, please run the operator certification workflow instead")
105+
}
106+
107+
// checking for policy exceptions
108+
override := lib.GetContainerPolicyExceptions(certProject)
91109

92110
c.policy = override
93111
}
@@ -217,4 +235,5 @@ type containerCheck struct {
217235
resolved bool
218236
policy policy.Policy
219237
konflux bool
238+
pyxisClient lib.PyxisClient // for testing purposes
220239
}

container/check_container_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
. "github.com/onsi/ginkgo/v2"
77
. "github.com/onsi/gomega"
88

9+
"github.com/redhat-openshift-ecosystem/openshift-preflight/internal/lib"
10+
911
"github.com/redhat-openshift-ecosystem/openshift-preflight/certification"
1012
preflighterr "github.com/redhat-openshift-ecosystem/openshift-preflight/errors"
1113
"github.com/redhat-openshift-ecosystem/openshift-preflight/internal/runtime"
@@ -147,4 +149,37 @@ var _ = Describe("Container Check Execution", func() {
147149
Expect(err).To(MatchError(preflighterr.ErrCannotResolvePolicyException))
148150
})
149151
})
152+
153+
When("checking for bundle projects", func() {
154+
It("should fail if the project is an operator bundle image", func() {
155+
fakeClient := &fakePyxisClient{
156+
getProjectFunc: returnBundleProject,
157+
}
158+
chk := NewCheck("placeholder", withPyxisClient(fakeClient))
159+
_, err := chk.Run(context.TODO())
160+
Expect(err).To(HaveOccurred())
161+
Expect(err.Error()).To(ContainSubstring("bundle project detected"))
162+
Expect(err.Error()).To(ContainSubstring("operator certification workflow"))
163+
})
164+
165+
It("should succeed if the project is a container project", func() {
166+
fakeClient := &fakePyxisClient{
167+
getProjectFunc: returnContainerProject,
168+
}
169+
goodImage := "quay.io/opdev/simple-demo-operator:latest"
170+
chk := NewCheck(goodImage, withPyxisClient(fakeClient))
171+
results, err := chk.Run(context.TODO())
172+
Expect(err).ToNot(HaveOccurred())
173+
Expect(results).ToNot(Equal(certification.Results{}))
174+
Expect(results.TestedImage).To(Equal(goodImage))
175+
})
176+
})
150177
})
178+
179+
// withPyxisClient injects a pyxis client for testing purposes.
180+
// This is not exported and should only be used in tests.
181+
func withPyxisClient(client lib.PyxisClient) Option {
182+
return func(cc *containerCheck) {
183+
cc.pyxisClient = client
184+
}
185+
}

container/fakes_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package container
2+
3+
import (
4+
"context"
5+
6+
"github.com/redhat-openshift-ecosystem/openshift-preflight/internal/pyxis"
7+
)
8+
9+
// fakePyxisClient implements lib.PyxisClient for testing
10+
type fakePyxisClient struct {
11+
getProjectFunc func(ctx context.Context) (*pyxis.CertProject, error)
12+
}
13+
14+
func (f *fakePyxisClient) GetProject(ctx context.Context) (*pyxis.CertProject, error) {
15+
if f.getProjectFunc != nil {
16+
return f.getProjectFunc(ctx)
17+
}
18+
return nil, nil
19+
}
20+
21+
func (f *fakePyxisClient) FindImagesByDigest(ctx context.Context, digests []string) ([]pyxis.CertImage, error) {
22+
return nil, nil
23+
}
24+
25+
func (f *fakePyxisClient) SubmitResults(ctx context.Context, certInput *pyxis.CertificationInput) (*pyxis.CertificationResults, error) {
26+
return nil, nil
27+
}
28+
29+
// Helper functions to return different project types
30+
func returnBundleProject(ctx context.Context) (*pyxis.CertProject, error) {
31+
return &pyxis.CertProject{
32+
Name: "test-bundle-project",
33+
Container: pyxis.Container{
34+
Type: "operator bundle image",
35+
},
36+
}, nil
37+
}
38+
39+
func returnContainerProject(ctx context.Context) (*pyxis.CertProject, error) {
40+
return &pyxis.CertProject{
41+
Name: "test-container-project",
42+
Container: pyxis.Container{
43+
Type: "container",
44+
},
45+
}, nil
46+
}

internal/lib/fakes_test.go

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -138,46 +138,6 @@ func gpFuncReturnScratchException(ctx context.Context) (*pyxis.CertProject, erro
138138
}, nil
139139
}
140140

141-
// gpFuncReturnScratchImageException implements gpFunc and returns a scratch image exception.
142-
func gpFuncReturnScratchImageException(ctx context.Context) (*pyxis.CertProject, error) {
143-
return &pyxis.CertProject{
144-
Container: pyxis.Container{
145-
OsContentType: "Scratch Image",
146-
},
147-
}, nil
148-
}
149-
150-
// gpFuncReturnRootException implements gpFunc and returns a root exception.
151-
func gpFuncReturnRootException(ctx context.Context) (*pyxis.CertProject, error) {
152-
return &pyxis.CertProject{
153-
Container: pyxis.Container{
154-
DockerConfigJSON: "",
155-
Privileged: true,
156-
},
157-
}, nil
158-
}
159-
160-
// gpFuncReturnScratchRootException implements gpFunc and returns a root exception.
161-
func gpFuncReturnScratchRootException(ctx context.Context) (*pyxis.CertProject, error) {
162-
return &pyxis.CertProject{
163-
Container: pyxis.Container{
164-
DockerConfigJSON: "",
165-
OsContentType: "Scratch Image",
166-
Privileged: true,
167-
},
168-
}, nil
169-
}
170-
171-
// gpFuncReturnNoException implements gpFunc and returns no exception indicators.
172-
func gpFuncReturnNoException(ctx context.Context) (*pyxis.CertProject, error) {
173-
return &pyxis.CertProject{
174-
Container: pyxis.Container{
175-
Type: "",
176-
Privileged: false,
177-
},
178-
}, nil
179-
}
180-
181141
// srFuncReturnError implements srFunc and returns a submission error.
182142
func srFuncReturnError(ctx context.Context, ci *pyxis.CertificationInput) (*pyxis.CertificationResults, error) {
183143
return nil, errors.New("some submission error")

internal/lib/lib.go

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
package lib
22

33
import (
4-
"context"
5-
"fmt"
6-
7-
"github.com/go-logr/logr"
8-
9-
"github.com/redhat-openshift-ecosystem/openshift-preflight/internal/log"
104
"github.com/redhat-openshift-ecosystem/openshift-preflight/internal/policy"
5+
"github.com/redhat-openshift-ecosystem/openshift-preflight/internal/pyxis"
116
)
127

138
// ResolveSubmitter will build out a ResultSubmitter if the provided pyxisClient, pc, is not nil.
@@ -25,36 +20,33 @@ func ResolveSubmitter(pc PyxisClient, projectID, dockerconfig, logfile string) R
2520
return NewNoopSubmitter(true, nil)
2621
}
2722

28-
// GetContainerPolicyExceptions will query Pyxis to determine if
29-
// a given project has a certification excemptions, such as root or scratch.
23+
// GetContainerPolicyExceptions accepts a CertProject to determine if
24+
// a given project has certification exemptions, such as root or scratch.
3025
// This will then return the corresponding policy.
3126
//
3227
// If no policy exception flags are found on the project, the standard
3328
// container policy is returned.
34-
func GetContainerPolicyExceptions(ctx context.Context, pc PyxisClient) (policy.Policy, error) {
35-
logger := logr.FromContextOrDiscard(ctx)
36-
37-
certProject, err := pc.GetProject(ctx)
38-
if err != nil {
39-
return "", fmt.Errorf("could not retrieve project: %w", err)
29+
func GetContainerPolicyExceptions(certProject *pyxis.CertProject) policy.Policy {
30+
// check for nil first so code doesn't panic later
31+
if certProject == nil {
32+
return policy.PolicyContainer
4033
}
41-
logger.V(log.DBG).Info("certification project", "name", certProject.Name)
4234

4335
// if the partner has gotten a scratch exception from the business and os_content_type == "Scratch Image"
4436
// and a partner sets `Host Level Access` in connect to `Privileged`, enable ScratchRootContainerPolicy checks
4537
if certProject.ScratchProject() && certProject.Container.Privileged {
46-
return policy.PolicyScratchRoot, nil
38+
return policy.PolicyScratchRoot
4739
}
4840

4941
// if the partner has gotten a scratch exception from the business and os_content_type == "Scratch Image",
5042
// enable ScratchNonRootContainerPolicy checks
5143
if certProject.ScratchProject() {
52-
return policy.PolicyScratchNonRoot, nil
44+
return policy.PolicyScratchNonRoot
5345
}
5446

5547
// if a partner sets `Host Level Access` in connect to `Privileged`, enable RootExceptionContainerPolicy checks
5648
if certProject.Container.Privileged {
57-
return policy.PolicyRoot, nil
49+
return policy.PolicyRoot
5850
}
59-
return policy.PolicyContainer, nil
51+
return policy.PolicyContainer
6052
}

internal/lib/types_test.go

Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -54,56 +54,63 @@ var _ = Describe("Pyxis Client Instantiation", func() {
5454

5555
var _ = Describe("Policy Resolution", func() {
5656
Context("When determining container policy exceptions", func() {
57-
var fakePC *FakePyxisClient
58-
BeforeEach(func() {
59-
// reset the fake pyxis client before each execution
60-
// as a precaution.
61-
fakePC = &FakePyxisClient{
62-
findImagesByDigestFunc: fidbFuncNoop,
63-
getProjectsFunc: gpFuncNoop,
64-
submitResultsFunc: srFuncNoop,
57+
It("should return a scratch policy exception if the project has type flag", func() {
58+
certProject := &pyxis.CertProject{
59+
Container: pyxis.Container{
60+
Type: "scratch",
61+
},
6562
}
66-
})
67-
68-
It("should throw an error if unable to get the project from the API", func() {
69-
fakePC.getProjectsFunc = gpFuncReturnError
70-
_, err := GetContainerPolicyExceptions(context.TODO(), fakePC)
71-
Expect(err).To(HaveOccurred())
72-
})
73-
74-
It("should return a scratch policy exception if the project has type flag in the API", func() {
75-
fakePC.getProjectsFunc = gpFuncReturnScratchException
76-
p, err := GetContainerPolicyExceptions(context.TODO(), fakePC)
63+
p := GetContainerPolicyExceptions(certProject)
7764
Expect(p).To(Equal(policy.PolicyScratchNonRoot))
78-
Expect(err).ToNot(HaveOccurred())
7965
})
8066

81-
It("should return a scratch policy exception if the project has os_content_type flag in the API", func() {
82-
fakePC.getProjectsFunc = gpFuncReturnScratchImageException
83-
p, err := GetContainerPolicyExceptions(context.TODO(), fakePC)
67+
It("should return a scratch policy exception if the project has os_content_type flag", func() {
68+
certProject := &pyxis.CertProject{
69+
Container: pyxis.Container{
70+
OsContentType: "Scratch Image",
71+
},
72+
}
73+
p := GetContainerPolicyExceptions(certProject)
8474
Expect(p).To(Equal(policy.PolicyScratchNonRoot))
85-
Expect(err).ToNot(HaveOccurred())
8675
})
8776

88-
It("should return a root policy exception if the project has the flag in the API", func() {
89-
fakePC.getProjectsFunc = gpFuncReturnRootException
90-
p, err := GetContainerPolicyExceptions(context.TODO(), fakePC)
77+
It("should return a root policy exception if the project has the flag", func() {
78+
certProject := &pyxis.CertProject{
79+
Container: pyxis.Container{
80+
DockerConfigJSON: "",
81+
Privileged: true,
82+
},
83+
}
84+
p := GetContainerPolicyExceptions(certProject)
9185
Expect(p).To(Equal(policy.PolicyRoot))
92-
Expect(err).ToNot(HaveOccurred())
9386
})
9487

95-
It("should return a scratch plus root policy exception if the project has the flag in the API", func() {
96-
fakePC.getProjectsFunc = gpFuncReturnScratchRootException
97-
p, err := GetContainerPolicyExceptions(context.TODO(), fakePC)
88+
It("should return a scratch plus root policy exception if the project has the flag", func() {
89+
certProject := &pyxis.CertProject{
90+
Container: pyxis.Container{
91+
DockerConfigJSON: "",
92+
OsContentType: "Scratch Image",
93+
Privileged: true,
94+
},
95+
}
96+
p := GetContainerPolicyExceptions(certProject)
9897
Expect(p).To(Equal(policy.PolicyScratchRoot))
99-
Expect(err).ToNot(HaveOccurred())
10098
})
10199

102-
It("should return a container policy exception if the project no exceptions in the API", func() {
103-
fakePC.getProjectsFunc = gpFuncReturnNoException
104-
p, err := GetContainerPolicyExceptions(context.TODO(), fakePC)
100+
It("should return a container policy exception if the project has no exceptions", func() {
101+
certProject := &pyxis.CertProject{
102+
Container: pyxis.Container{
103+
Type: "",
104+
Privileged: false,
105+
},
106+
}
107+
p := GetContainerPolicyExceptions(certProject)
108+
Expect(p).To(Equal(policy.PolicyContainer))
109+
})
110+
111+
It("should return a container policy exception if called with nil container project", func() {
112+
p := GetContainerPolicyExceptions(nil)
105113
Expect(p).To(Equal(policy.PolicyContainer))
106-
Expect(err).ToNot(HaveOccurred())
107114
})
108115
})
109116
})

internal/pyxis/types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ func (cp CertProject) ScratchProject() bool {
109109
return cp.Container.Type == "scratch" || cp.Container.OsContentType == "Scratch Image"
110110
}
111111

112+
func (cp CertProject) BundleProject() bool {
113+
// BundleProject returns true if the CertProject is designated as a Bundle in Pyxis.
114+
return cp.Container.Type == "operator bundle image"
115+
}
116+
112117
type Container struct {
113118
DockerConfigJSON string `json:"docker_config_json,omitempty"`
114119
HostedRegistry bool `json:"hosted_registry,omitempty"`

0 commit comments

Comments
 (0)