Skip to content

Commit e66ff5b

Browse files
committed
[MCO-1956] Extract image utils from build package
This change extracts some containers/image common logic to a dedicated image utils package to improve reusability.
1 parent 9db4a37 commit e66ff5b

File tree

6 files changed

+270
-320
lines changed

6 files changed

+270
-320
lines changed

pkg/controller/build/imagepruner/imageinspect.go

Lines changed: 11 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@ package imagepruner
33
import (
44
"context"
55
"fmt"
6-
"strings"
76

87
"github.com/containers/common/pkg/retry"
9-
"github.com/containers/image/v5/docker"
108
"github.com/containers/image/v5/image"
119
"github.com/containers/image/v5/manifest"
1210
"github.com/containers/image/v5/types"
1311
digest "github.com/opencontainers/go-digest"
12+
"github.com/openshift/machine-config-operator/pkg/imageutils"
1413
)
1514

1615
const (
@@ -58,24 +57,6 @@ func (i *imageInspectorImpl) DeleteImage(ctx context.Context, sysCtx *types.Syst
5857
return deleteImage(ctx, sysCtx, image)
5958
}
6059

61-
// parseImageName parses the image name string into an ImageReference,
62-
// handling various prefix formats like "docker://" and ensuring a standard format.
63-
func parseImageName(imgName string) (types.ImageReference, error) {
64-
if strings.Contains(imgName, "//") && !strings.HasPrefix(imgName, "docker://") {
65-
return nil, fmt.Errorf("unknown transport for pullspec %s", imgName)
66-
}
67-
68-
if strings.HasPrefix(imgName, "docker://") {
69-
imgName = strings.ReplaceAll(imgName, "docker://", "//")
70-
}
71-
72-
if !strings.HasPrefix(imgName, "//") {
73-
imgName = "//" + imgName
74-
}
75-
76-
return docker.Transport.ParseReference(imgName)
77-
}
78-
7960
// deleteImage attempts to delete the specified image with retries,
8061
// using the provided context and system context.
8162
//
@@ -85,7 +66,7 @@ func deleteImage(ctx context.Context, sysCtx *types.SystemContext, imageName str
8566
MaxRetry: cmdRetriesCount,
8667
}
8768

88-
ref, err := parseImageName(imageName)
69+
ref, err := imageutils.ParseImageName(imageName)
8970
if err != nil {
9071
return err
9172
}
@@ -105,29 +86,19 @@ func deleteImage(ctx context.Context, sysCtx *types.SystemContext, imageName str
10586
//
10687
//nolint:unparam
10788
func imageInspect(ctx context.Context, sysCtx *types.SystemContext, imageName string) (*types.ImageInspectInfo, *digest.Digest, error) {
108-
var (
109-
src types.ImageSource
110-
imgInspect *types.ImageInspectInfo
111-
err error
112-
)
89+
ref, err := imageutils.ParseImageName(imageName)
90+
if err != nil {
91+
return nil, nil, fmt.Errorf("error parsing image name %q: %w", imageName, err)
92+
}
11393

11494
retryOpts := retry.RetryOptions{
11595
MaxRetry: cmdRetriesCount,
11696
}
117-
118-
ref, err := parseImageName(imageName)
97+
src, err := imageutils.GetImageSourceFromReference(ctx, sysCtx, ref, &retryOpts)
11998
if err != nil {
120-
return nil, nil, fmt.Errorf("error parsing image name %q: %w", imageName, err)
121-
}
122-
123-
// retry.IfNecessary takes into account whether the error is "retryable"
124-
// so we don't keep looping on errors that will never resolve
125-
if err := retry.RetryIfNecessary(ctx, func() error {
126-
src, err = ref.NewImageSource(ctx, sysCtx)
127-
return err
128-
}, &retryOpts); err != nil {
129-
return nil, nil, newErrImage(imageName, fmt.Errorf("error getting image source: %w", err))
99+
return nil, nil, fmt.Errorf("error getting image source for %s: %w", imageName, err)
130100
}
101+
defer src.Close()
131102

132103
var rawManifest []byte
133104
unparsedInstance := image.UnparsedInstance(src, nil)
@@ -144,14 +115,13 @@ func imageInspect(ctx context.Context, sysCtx *types.SystemContext, imageName st
144115
return nil, nil, fmt.Errorf("error retrieving image digest: %q: %w", imageName, err)
145116
}
146117

147-
defer src.Close()
148-
149118
img, err := image.FromUnparsedImage(ctx, sysCtx, unparsedInstance)
150119
if err != nil {
151120
return nil, nil, newErrImage(imageName, fmt.Errorf("error parsing manifest for image: %w", err))
152121
}
153122

154-
if err := retry.RetryIfNecessary(ctx, func() error {
123+
var imgInspect *types.ImageInspectInfo
124+
if err := retry.IfNecessary(ctx, func() error {
155125
imgInspect, err = img.Inspect(ctx)
156126
return err
157127
}, &retryOpts); err != nil {

pkg/controller/build/imagepruner/imagepruner.go

Lines changed: 7 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,14 @@ package imagepruner
33
import (
44
"context"
55
"fmt"
6-
"os"
7-
"path/filepath"
8-
"strings"
96

7+
"github.com/openshift/machine-config-operator/pkg/imageutils"
108
corev1 "k8s.io/api/core/v1"
119
"k8s.io/klog/v2"
1210

1311
"github.com/containers/image/v5/types"
1412
"github.com/opencontainers/go-digest"
1513
mcfgv1 "github.com/openshift/api/machineconfiguration/v1"
16-
"github.com/openshift/machine-config-operator/pkg/controller/template"
17-
"github.com/openshift/machine-config-operator/pkg/secrets"
1814
)
1915

2016
// ImagePruner defines the interface for inspecting and deleting container images.
@@ -42,18 +38,18 @@ func NewImagePruner() ImagePruner {
4238
// InspectImage inspects the given image using the provided secret. It also accepts a
4339
// ControllerConfig so that certificates may be placed on the filesystem for authentication.
4440
func (i *imagePrunerImpl) InspectImage(ctx context.Context, pullspec string, secret *corev1.Secret, cc *mcfgv1.ControllerConfig) (*types.ImageInspectInfo, *digest.Digest, error) {
45-
sysCtx, err := i.prepareSystemContext(secret, cc)
41+
sysCtx, err := imageutils.NewSysContextFromControllerConfig(secret, cc)
4642
if err != nil {
4743
return nil, nil, fmt.Errorf("could not prepare for image inspection: %w", err)
4844
}
4945

5046
defer func() {
51-
if err := i.cleanup(sysCtx); err != nil {
47+
if err := sysCtx.Cleanup(); err != nil {
5248
klog.Warningf("Unable to clean up after inspection of %s: %s", pullspec, err)
5349
}
5450
}()
5551

56-
info, digest, err := i.images.ImageInspect(ctx, sysCtx, pullspec)
52+
info, digest, err := i.images.ImageInspect(ctx, sysCtx.SysContext, pullspec)
5753
if err != nil {
5854
return nil, nil, fmt.Errorf("could not inspect image: %w", err)
5955
}
@@ -64,158 +60,20 @@ func (i *imagePrunerImpl) InspectImage(ctx context.Context, pullspec string, sec
6460
// DeleteImage deletes the given image using the provided secret. It also accepts a
6561
// ControllerConfig so that certificates may be placed on the filesystem for authentication.
6662
func (i *imagePrunerImpl) DeleteImage(ctx context.Context, pullspec string, secret *corev1.Secret, cc *mcfgv1.ControllerConfig) error {
67-
sysCtx, err := i.prepareSystemContext(secret, cc)
63+
sysCtx, err := imageutils.NewSysContextFromControllerConfig(secret, cc)
6864
if err != nil {
6965
return fmt.Errorf("could not prepare for image deletion: %w", err)
7066
}
7167

7268
defer func() {
73-
if err := i.cleanup(sysCtx); err != nil {
69+
if err := sysCtx.Cleanup(); err != nil {
7470
klog.Warningf("Unable to clean up after deletion of %s: %s", pullspec, err)
7571
}
7672
}()
7773

78-
if err := i.images.DeleteImage(ctx, sysCtx, pullspec); err != nil {
74+
if err := i.images.DeleteImage(ctx, sysCtx.SysContext, pullspec); err != nil {
7975
return fmt.Errorf("could not delete image: %w", err)
8076
}
8177

8278
return nil
8379
}
84-
85-
// prepareSystemContext prepares to perform the requested operation by first creating the
86-
// certificate directory and then writing the authfile to the appropriate path.
87-
func (i *imagePrunerImpl) prepareSystemContext(secret *corev1.Secret, cc *mcfgv1.ControllerConfig) (*types.SystemContext, error) {
88-
// Make a deep copy of the ControllerConfig because the write process mutates
89-
// the ControllerConfig in-place.
90-
certsDir, err := i.prepareCerts(cc.DeepCopy())
91-
if err != nil {
92-
return nil, fmt.Errorf("could not prepare certs: %w", err)
93-
}
94-
95-
authfilePath, err := i.prepareAuthfile(secret)
96-
if err != nil {
97-
return nil, fmt.Errorf("could not get authfile path for secret %s: %w", secret.Name, err)
98-
}
99-
100-
return &types.SystemContext{
101-
AuthFilePath: authfilePath,
102-
DockerPerHostCertDirPath: certsDir,
103-
}, nil
104-
}
105-
106-
// cleanup cleans up after an operation by removing the temporary certificates directory
107-
// and the temporary authfile.
108-
func (i *imagePrunerImpl) cleanup(sysCtx *types.SystemContext) error {
109-
if err := os.RemoveAll(sysCtx.DockerPerHostCertDirPath); err != nil && !os.IsNotExist(err) {
110-
return fmt.Errorf("could not clean up certs directory %s: %w", sysCtx.DockerPerHostCertDirPath, err)
111-
}
112-
113-
if err := os.RemoveAll(sysCtx.AuthFilePath); err != nil && !os.IsNotExist(err) {
114-
return fmt.Errorf("could not clean up authfile directory %s: %w", sysCtx.AuthFilePath, err)
115-
}
116-
117-
return nil
118-
}
119-
120-
// prepareCerts prepares the certificates by first creating a temporary directory for them
121-
// and then writing the certs from the ControllerConfig to that directory.
122-
func (i *imagePrunerImpl) prepareCerts(cc *mcfgv1.ControllerConfig) (string, error) {
123-
certsDir, err := os.MkdirTemp("", "imagepruner-certs-dir")
124-
if err != nil {
125-
return "", fmt.Errorf("could not create temp dir: %w", err)
126-
}
127-
128-
if err := i.writeCerts(certsDir, cc); err != nil {
129-
return "", fmt.Errorf("could not write certs: %w", err)
130-
}
131-
132-
return certsDir, nil
133-
}
134-
135-
// writeCerts extracts the certificates from the ControllerConfig and writes them
136-
// to the appropriate directory, which defaults to /etc/docker/certs.d.
137-
func (i *imagePrunerImpl) writeCerts(certsDir string, cc *mcfgv1.ControllerConfig) error {
138-
template.UpdateControllerConfigCerts(cc)
139-
140-
for _, irb := range cc.Spec.ImageRegistryBundleData {
141-
if err := i.writeCertFromImageRegistryBundle(certsDir, irb); err != nil {
142-
return fmt.Errorf("could not write image registry bundle from ImageRegistryBundleData: %w", err)
143-
}
144-
}
145-
146-
for _, irb := range cc.Spec.ImageRegistryBundleUserData {
147-
if err := i.writeCertFromImageRegistryBundle(certsDir, irb); err != nil {
148-
return fmt.Errorf("could not write image registry bundle from ImageRegistryBundleUserData: %w", err)
149-
}
150-
}
151-
152-
return nil
153-
}
154-
155-
// writeCertFromImageRegistryBundle writes a certificate from an image registry bundle
156-
// to the specified certificates directory, creating necessary subdirectories.
157-
func (i *imagePrunerImpl) writeCertFromImageRegistryBundle(certsDir string, irb mcfgv1.ImageRegistryBundle) error {
158-
caFile := strings.ReplaceAll(irb.File, "..", ":")
159-
160-
certDir := filepath.Join(certsDir, caFile)
161-
162-
if err := os.MkdirAll(certDir, 0o755); err != nil {
163-
return fmt.Errorf("could not create cert dir %q: %w", certDir, err)
164-
}
165-
166-
certFile := filepath.Join(certDir, "ca.crt")
167-
168-
if err := os.WriteFile(certFile, irb.Data, 0o644); err != nil {
169-
return fmt.Errorf("could not write cert file %q: %w", certFile, err)
170-
}
171-
172-
return nil
173-
}
174-
175-
// prepareAuthfile creates a temporary directory and writes the Docker secret
176-
// (authfile) into a file named "authfile.json" within that directory.
177-
// It returns the path to the created authfile.
178-
func (i *imagePrunerImpl) prepareAuthfile(secret *corev1.Secret) (string, error) {
179-
authfileDir, err := os.MkdirTemp("", "imagepruner-authfile")
180-
if err != nil {
181-
return "", fmt.Errorf("could not create temp dir for authfile: %w", err)
182-
}
183-
184-
authfilePath := filepath.Join(authfileDir, "authfile.json")
185-
186-
is, err := secrets.NewImageRegistrySecret(secret)
187-
if err != nil {
188-
return "", fmt.Errorf("could not create an ImageRegistrySecret for '%s/%s': %w", secret.Namespace, secret.Name, err)
189-
}
190-
191-
secretBytes, err := is.JSONBytes(corev1.SecretTypeDockerConfigJson)
192-
if err != nil {
193-
return "", fmt.Errorf("could not normalize secret '%s/%s' to %s: %w", secret.Namespace, secret.Name, corev1.SecretTypeDockerConfigJson, err)
194-
}
195-
196-
if err := os.WriteFile(authfilePath, secretBytes, 0o644); err != nil {
197-
return "", fmt.Errorf("could not write temp authfile %q for secret %q: %w", authfilePath, secret.Name, err)
198-
}
199-
200-
return authfilePath, nil
201-
}
202-
203-
// writeAuthfile ensures that the image registry secret is in the dockerconfigjson format
204-
// and writes it to the specified path.
205-
func (i *imagePrunerImpl) writeAuthfile(secret *corev1.Secret, authfilePath string) error {
206-
is, err := secrets.NewImageRegistrySecret(secret)
207-
if err != nil {
208-
return fmt.Errorf("could not create an ImageRegistrySecret for '%s/%s': %w", secret.Namespace, secret.Name, err)
209-
}
210-
211-
secretBytes, err := is.JSONBytes(corev1.SecretTypeDockerConfigJson)
212-
if err != nil {
213-
return fmt.Errorf("could not normalize secret '%s/%s' to %s: %w", secret.Namespace, secret.Name, corev1.SecretTypeDockerConfigJson, err)
214-
}
215-
216-
if err := os.WriteFile(authfilePath, secretBytes, 0o644); err != nil {
217-
return fmt.Errorf("could not write temp authfile %q for secret %q: %w", authfilePath, secret.Name, err)
218-
}
219-
220-
return nil
221-
}

0 commit comments

Comments
 (0)