• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sapcc / keppel / 13332708124

14 Feb 2025 03:52PM UTC coverage: 80.546%. Remained the same
13332708124

push

github

web-flow
Merge pull request #495 from sapcc/fix-validate

fix `keppel validate` failing if a reference without digest is given

0 of 1 new or added line in 1 file covered. (0.0%)

7312 of 9078 relevant lines covered (80.55%)

155.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/internal/client/validate.go
1
/******************************************************************************
2
*
3
*  Copyright 2020 SAP SE
4
*
5
*  Licensed under the Apache License, Version 2.0 (the "License");
6
*  you may not use this file except in compliance with the License.
7
*  You may obtain a copy of the License at
8
*
9
*      http://www.apache.org/licenses/LICENSE-2.0
10
*
11
*  Unless required by applicable law or agreed to in writing, software
12
*  distributed under the License is distributed on an "AS IS" BASIS,
13
*  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
*  See the License for the specific language governing permissions and
15
*  limitations under the License.
16
*
17
******************************************************************************/
18

19
package client
20

21
import (
22
        "context"
23
        "fmt"
24
        "io"
25

26
        "github.com/opencontainers/go-digest"
27

28
        "github.com/sapcc/keppel/internal/keppel"
29
        "github.com/sapcc/keppel/internal/models"
30
)
31

32
// ValidationLogger can be passed to ValidateManifest, primarily to allow the
33
// caller to log the progress of the validation operation.
34
type ValidationLogger interface {
35
        LogManifest(reference models.ManifestReference, level int, validationResult error, resultFromCache bool)
36
        LogBlob(d digest.Digest, level int, validationResult error, resultFromCache bool)
37
}
38

39
type noopLogger struct{}
40

41
func (noopLogger) LogManifest(models.ManifestReference, int, error, bool) {}
×
42
func (noopLogger) LogBlob(digest.Digest, int, error, bool)                {}
×
43

44
// ValidationSession holds state and caches intermediate results over the
45
// course of several ValidateManifest() and ValidateBlobContents() calls.
46
// The cache optimizes the validation of submanifests and blobs that are
47
// referenced multiple times. The session instance should only be used for as
48
// long as the caller wishes to cache validation results.
49
type ValidationSession struct {
50
        Logger  ValidationLogger
51
        isValid map[string]bool
52
}
53

54
func (s *ValidationSession) applyDefaults() *ValidationSession {
×
55
        if s == nil {
×
56
                // This branch is taken when the caller supplied `nil` for the
×
57
                // *ValidationSession argument in ValidateManifest or ValidateBlobContents.
×
58
                s = &ValidationSession{}
×
59
        }
×
60
        if s.Logger == nil {
×
61
                s.Logger = noopLogger{}
×
62
        }
×
63
        if s.isValid == nil {
×
64
                s.isValid = make(map[string]bool)
×
65
        }
×
66
        return s
×
67
}
68

69
func (c *RepoClient) validationCacheKey(digestOrTagName string) string {
×
70
        // We allow sharing a ValidationSession between multiple RepoClients to keep
×
71
        // the API simple. But we cannot share validation results between repos: For
×
72
        // any given digest, validation could succeed in one repo, fail in a second
×
73
        // repo, and fail *in a different way* in the third repo. Therefore we need
×
74
        // to store validation results keyed by digest *and* repo URL.
×
75
        return fmt.Sprintf("%s/%s/%s", c.Host, c.RepoName, digestOrTagName)
×
76
}
×
77

78
// ValidateManifest fetches the given manifest from the repo and verifies that
79
// it parses correctly. It also validates all references manifests and blobs
80
// recursively.
81
func (c *RepoClient) ValidateManifest(ctx context.Context, reference models.ManifestReference, session *ValidationSession, platformFilter models.PlatformFilter) error {
×
82
        return c.doValidateManifest(ctx, reference, 0, session.applyDefaults(), platformFilter)
×
83
}
×
84

85
func (c *RepoClient) doValidateManifest(ctx context.Context, reference models.ManifestReference, level int, session *ValidationSession, platformFilter models.PlatformFilter) (returnErr error) {
×
86
        if session.isValid[c.validationCacheKey(reference.String())] {
×
87
                session.Logger.LogManifest(reference, level, nil, true)
×
88
                return nil
×
89
        }
×
90

91
        logged := false
×
92
        defer func() {
×
93
                if !logged {
×
94
                        session.Logger.LogManifest(reference, level, returnErr, false)
×
95
                }
×
96
        }()
97

98
        manifestBytes, manifestMediaType, err := c.DownloadManifest(ctx, reference, nil)
×
99
        if err != nil {
×
100
                return err
×
101
        }
×
102
        manifest, err := keppel.ParseManifest(manifestMediaType, manifestBytes)
×
103
        if err != nil {
×
104
                return err
×
105
        }
×
106

107
        digest := digest.FromBytes(manifestBytes)
×
108

×
NEW
109
        if reference.Digest != "" && digest != reference.Digest {
×
110
                return keppel.ErrDigestInvalid.With("actual manifest digest is " + digest.String())
×
111
        }
×
112

113
        // the manifest itself looks good...
114
        session.Logger.LogManifest(models.ManifestReference{Digest: digest}, level, nil, false)
×
115
        logged = true
×
116

×
117
        // ...now recurse into the manifests and blobs that it references
×
118
        for _, layerInfo := range manifest.BlobReferences() {
×
119
                err := c.doValidateBlobContents(ctx, layerInfo.Digest, level+1, session)
×
120
                if err != nil {
×
121
                        return err
×
122
                }
×
123
        }
124
        for _, desc := range manifest.ManifestReferences(platformFilter) {
×
125
                err := c.doValidateManifest(ctx, models.ManifestReference{Digest: desc.Digest}, level+1, session, platformFilter)
×
126
                if err != nil {
×
127
                        return err
×
128
                }
×
129
        }
130

131
        // write validity into cache only after all references have been validated as well
132
        session.isValid[c.validationCacheKey(digest.String())] = true
×
133
        session.isValid[c.validationCacheKey(reference.String())] = true
×
134
        return nil
×
135
}
136

137
// ValidateBlobContents fetches the given blob from the repo and verifies that
138
// the contents produce the correct digest.
139
func (c *RepoClient) ValidateBlobContents(ctx context.Context, blobDigest digest.Digest, session *ValidationSession) error {
×
140
        return c.doValidateBlobContents(ctx, blobDigest, 0, session.applyDefaults())
×
141
}
×
142

143
func (c *RepoClient) doValidateBlobContents(ctx context.Context, blobDigest digest.Digest, level int, session *ValidationSession) (returnErr error) {
×
144
        cacheKey := c.validationCacheKey(blobDigest.String())
×
145
        if session.isValid[cacheKey] {
×
146
                session.Logger.LogBlob(blobDigest, level, nil, true)
×
147
                return nil
×
148
        }
×
149
        defer func() {
×
150
                session.Logger.LogBlob(blobDigest, level, returnErr, false)
×
151
        }()
×
152

153
        readCloser, _, err := c.DownloadBlob(ctx, blobDigest)
×
154
        if err != nil {
×
155
                return err
×
156
        }
×
157

158
        defer func() {
×
159
                if returnErr == nil {
×
160
                        returnErr = readCloser.Close()
×
161
                } else {
×
162
                        readCloser.Close()
×
163
                }
×
164
        }()
165

166
        hash := blobDigest.Algorithm().Hash()
×
167
        _, err = io.Copy(hash, readCloser)
×
168
        if err != nil {
×
169
                return err
×
170
        }
×
171
        actualDigest := digest.NewDigest(blobDigest.Algorithm(), hash)
×
172
        if actualDigest != blobDigest {
×
173
                return fmt.Errorf("actual digest is %s", actualDigest)
×
174
        }
×
175

176
        session.isValid[cacheKey] = true
×
177
        return nil
×
178
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc