|
| 1 | +package attest_test |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "encoding/json" |
| 6 | + "fmt" |
| 7 | + "os" |
| 8 | + "path/filepath" |
| 9 | + "testing" |
| 10 | + |
| 11 | + gogit "github.com/go-git/go-git/v5" |
| 12 | + "github.com/go-git/go-git/v5/plumbing" |
| 13 | + "github.com/google/go-containerregistry/pkg/name" |
| 14 | + |
| 15 | + . "github.com/onsi/gomega" |
| 16 | + |
| 17 | + . "github.com/errordeveloper/tape/attest" |
| 18 | + "github.com/errordeveloper/tape/attest/vcs/git" |
| 19 | + "github.com/errordeveloper/tape/manifest/imagescanner" |
| 20 | + "github.com/errordeveloper/tape/manifest/loader" |
| 21 | + "github.com/errordeveloper/tape/oci" |
| 22 | +) |
| 23 | + |
| 24 | +type vcsTestCase struct { |
| 25 | + URL, CheckoutTag, CheckoutHash, Branch string |
| 26 | + LoadPath string |
| 27 | + ExpectManifests, ExpectImageTags, ExpectRawTags []string |
| 28 | +} |
| 29 | + |
| 30 | +func (tc vcsTestCase) Name() string { |
| 31 | + rev := tc.CheckoutTag |
| 32 | + if rev == "" { |
| 33 | + rev = tc.CheckoutHash |
| 34 | + } |
| 35 | + return fmt.Sprintf("%s@%s", tc.URL, rev) |
| 36 | +} |
| 37 | + |
| 38 | +func TestVCS(t *testing.T) { |
| 39 | + testCases := []vcsTestCase{ |
| 40 | + // { |
| 41 | + // URL: "https://github.com/stefanprodan/podinfo", |
| 42 | + // CheckoutTag: "6.7.0", // => 0b1481aa8ed0a6c34af84f779824a74200d5c1d6 |
| 43 | + // LoadPath: "kustomize", |
| 44 | + // ExpectManifests: []string{"kustomization.yaml", "deployment.yaml", "hpa.yaml", "service.yaml"}, |
| 45 | + // ExpectImageTags: []string{"6.7.0"}, |
| 46 | + // ExpectRawTags: []string{"6.7.0"}, |
| 47 | + // }, |
| 48 | + // { |
| 49 | + // URL: "https://github.com/stefanprodan/podinfo", |
| 50 | + // CheckoutHash: "0b1481aa8ed0a6c34af84f779824a74200d5c1d6", // => 6.7.0 |
| 51 | + // Branch: "master", |
| 52 | + // LoadPath: "kustomize", |
| 53 | + // ExpectManifests: []string{"kustomization.yaml", "deployment.yaml", "hpa.yaml", "service.yaml"}, |
| 54 | + // ExpectImageTags: []string{"6.7.0"}, |
| 55 | + // ExpectRawTags: []string{"6.7.0"}, |
| 56 | + // }, |
| 57 | + { |
| 58 | + URL: "https://github.com/errordeveloper/tape-git-testing", |
| 59 | + CheckoutHash: "3cad1d255c1d83b5e523de64d34758609498d81b", |
| 60 | + Branch: "main", |
| 61 | + LoadPath: "", |
| 62 | + ExpectManifests: []string{"kustomization.yaml", "deployment.yaml", "hpa.yaml", "service.yaml"}, |
| 63 | + ExpectImageTags: nil, |
| 64 | + ExpectRawTags: nil, |
| 65 | + }, |
| 66 | + { |
| 67 | + URL: "https://github.com/errordeveloper/tape-git-testing", |
| 68 | + CheckoutTag: "0.0.1", |
| 69 | + LoadPath: "", |
| 70 | + ExpectManifests: []string{"podinfo/kustomization.yaml", "podinfo/deployment.yaml", "podinfo/hpa.yaml", "podinfo/service.yaml"}, |
| 71 | + ExpectImageTags: []string{"v0.0.1"}, |
| 72 | + ExpectRawTags: []string{"0.0.1", "v0.0.1", "podinfo/v6.6.3"}, |
| 73 | + }, |
| 74 | + { |
| 75 | + URL: "https://github.com/errordeveloper/tape-git-testing", |
| 76 | + CheckoutTag: "v0.0.2", |
| 77 | + LoadPath: "podinfo", |
| 78 | + ExpectManifests: []string{"kustomization.yaml", "deployment.yaml", "hpa.yaml", "service.yaml"}, |
| 79 | + ExpectImageTags: []string{"v6.7.0"}, |
| 80 | + ExpectRawTags: []string{"0.0.2", "v0.0.2", "podinfo/v6.7.0"}, |
| 81 | + }, |
| 82 | + { |
| 83 | + URL: "https://github.com/errordeveloper/tape-git-testing", |
| 84 | + CheckoutHash: "9eeeed9f4ff44812ca23dba1bd0af9f509686d21", // => v0.0.1 |
| 85 | + LoadPath: "podinfo", |
| 86 | + ExpectManifests: []string{"kustomization.yaml", "deployment.yaml", "hpa.yaml", "service.yaml"}, |
| 87 | + ExpectImageTags: []string{"v6.6.3"}, |
| 88 | + ExpectRawTags: []string{"0.0.1", "v0.0.1", "podinfo/v6.6.3"}, |
| 89 | + }, |
| 90 | + } |
| 91 | + |
| 92 | + repos := &repos{} |
| 93 | + repos.init() |
| 94 | + defer repos.cleanup() |
| 95 | + |
| 96 | + for i := range testCases { |
| 97 | + tc := testCases[i] |
| 98 | + t.Run(tc.Name(), makeVCSTest(repos, tc)) |
| 99 | + } |
| 100 | +} |
| 101 | + |
| 102 | +func makeVCSTest(repos *repos, tc vcsTestCase) func(t *testing.T) { |
| 103 | + return func(t *testing.T) { |
| 104 | + g := NewWithT(t) |
| 105 | + |
| 106 | + ctx := context.Background() |
| 107 | + checkoutPath, err := repos.clone(ctx, tc) |
| 108 | + g.Expect(err).NotTo(HaveOccurred()) |
| 109 | + |
| 110 | + loadPath := filepath.Join(checkoutPath, tc.LoadPath) |
| 111 | + loader := loader.NewRecursiveManifestDirectoryLoader(loadPath) |
| 112 | + g.Expect(loader.Load()).To(Succeed()) |
| 113 | + |
| 114 | + repoDetected, attreg, err := DetectVCS(loadPath) |
| 115 | + g.Expect(err).NotTo(HaveOccurred()) |
| 116 | + g.Expect(repoDetected).To(BeTrue()) |
| 117 | + g.Expect(attreg).ToNot(BeNil()) |
| 118 | + |
| 119 | + scanner := imagescanner.NewDefaultImageScanner() |
| 120 | + scanner.WithProvinanceAttestor(attreg) |
| 121 | + |
| 122 | + if tc.ExpectManifests != nil { |
| 123 | + g.Expect(loader.Paths()).To(HaveLen(len(tc.ExpectManifests))) |
| 124 | + for _, manifest := range tc.ExpectManifests { |
| 125 | + g.Expect(loader.ContainsRelPath(manifest)).To(BeTrue()) |
| 126 | + } |
| 127 | + } |
| 128 | + |
| 129 | + g.Expect(scanner.Scan(loader.RelPaths())).To(Succeed()) |
| 130 | + |
| 131 | + collection, err := attreg.MakePathCheckSummarySummaryCollection() |
| 132 | + g.Expect(err).NotTo(HaveOccurred()) |
| 133 | + g.Expect(collection).ToNot(BeNil()) |
| 134 | + g.Expect(collection.Providers).To(ConsistOf("git")) |
| 135 | + g.Expect(collection.EntryGroups).To(HaveLen(1)) |
| 136 | + g.Expect(collection.EntryGroups[0]).To(HaveLen(5)) |
| 137 | + |
| 138 | + vcsSummary := attreg.BaseDirSummary() |
| 139 | + g.Expect(vcsSummary).ToNot(BeNil()) |
| 140 | + summaryJSON, err := json.Marshal(vcsSummary.Full()) |
| 141 | + g.Expect(err).NotTo(HaveOccurred()) |
| 142 | + t.Logf("VCS info for %q: %s", tc.LoadPath, summaryJSON) |
| 143 | + |
| 144 | + g.Expect(attreg.AssociateCoreStatements()).To(Succeed()) |
| 145 | + |
| 146 | + statements := attreg.GetStatements() |
| 147 | + g.Expect(statements).To(HaveLen(1)) |
| 148 | + g.Expect(statements[0].GetSubject()).To(HaveLen(4)) |
| 149 | + |
| 150 | + // TODO: validate schema |
| 151 | + |
| 152 | + groupSummary, ok := vcsSummary.Full().(*git.Summary) |
| 153 | + g.Expect(ok).To(BeTrue()) |
| 154 | + ref := groupSummary.Git.Reference |
| 155 | + g.Expect(ref.Tags).To(HaveLen(len(tc.ExpectRawTags))) |
| 156 | + imageTagNames := make([]string, len(ref.Tags)) |
| 157 | + for i, tag := range ref.Tags { |
| 158 | + imageTagNames[i] = tag.Name |
| 159 | + } |
| 160 | + g.Expect(imageTagNames).To(ConsistOf(tc.ExpectRawTags)) |
| 161 | + |
| 162 | + image, err := name.NewRepository("podinfo") |
| 163 | + g.Expect(err).NotTo(HaveOccurred()) |
| 164 | + |
| 165 | + semVerTags := oci.SemVerTagsFromAttestations(ctx, image.Tag("test.123456"), statements...) |
| 166 | + g.Expect(semVerTags).To(HaveLen(len(tc.ExpectImageTags))) |
| 167 | + semVerTagNames := make([]string, len(semVerTags)) |
| 168 | + for i, tag := range semVerTags { |
| 169 | + semVerTagNames[i] = tag.TagStr() |
| 170 | + } |
| 171 | + g.Expect(semVerTagNames).To(ConsistOf(tc.ExpectImageTags)) |
| 172 | + } |
| 173 | +} |
| 174 | + |
| 175 | +type repos struct { |
| 176 | + workDir string |
| 177 | + tempDir string |
| 178 | + cache map[string]string |
| 179 | +} |
| 180 | + |
| 181 | +func (r *repos) init() error { |
| 182 | + workDir, err := os.Getwd() |
| 183 | + if err != nil { |
| 184 | + return err |
| 185 | + } |
| 186 | + r.workDir = workDir |
| 187 | + tempDir, err := os.MkdirTemp("", ".vcs-test-*") |
| 188 | + if err != nil { |
| 189 | + return err |
| 190 | + } |
| 191 | + r.tempDir = tempDir |
| 192 | + r.cache = map[string]string{} |
| 193 | + return nil |
| 194 | +} |
| 195 | + |
| 196 | +func (r *repos) cleanup() error { |
| 197 | + if r.tempDir == "" { |
| 198 | + return nil |
| 199 | + } |
| 200 | + return os.RemoveAll(r.tempDir) |
| 201 | +} |
| 202 | + |
| 203 | +func (r *repos) mktemp() (string, error) { |
| 204 | + return os.MkdirTemp(r.tempDir, "repo-*") |
| 205 | +} |
| 206 | + |
| 207 | +func (r *repos) mirror(ctx context.Context, tc vcsTestCase) (string, error) { |
| 208 | + if _, ok := r.cache[tc.URL]; !ok { |
| 209 | + mirrorDir, err := r.mktemp() |
| 210 | + if err != nil { |
| 211 | + return "", err |
| 212 | + } |
| 213 | + _, err = gogit.PlainCloneContext(ctx, mirrorDir, true, &gogit.CloneOptions{Mirror: true, URL: tc.URL}) |
| 214 | + if err != nil { |
| 215 | + return "", err |
| 216 | + } |
| 217 | + r.cache[tc.URL] = mirrorDir |
| 218 | + } |
| 219 | + return r.cache[tc.URL], nil |
| 220 | +} |
| 221 | + |
| 222 | +func (r *repos) clone(ctx context.Context, tc vcsTestCase) (string, error) { |
| 223 | + mirrorDir, err := r.mirror(ctx, tc) |
| 224 | + if err != nil { |
| 225 | + return "", err |
| 226 | + } |
| 227 | + checkoutDir, err := r.mktemp() |
| 228 | + if err != nil { |
| 229 | + return "", err |
| 230 | + } |
| 231 | + |
| 232 | + opts := &gogit.CloneOptions{URL: mirrorDir} |
| 233 | + if tc.CheckoutTag != "" { |
| 234 | + opts.ReferenceName = plumbing.NewTagReferenceName(tc.CheckoutTag) |
| 235 | + } else if tc.Branch != "" { |
| 236 | + opts.ReferenceName = plumbing.NewBranchReferenceName(tc.Branch) |
| 237 | + } |
| 238 | + |
| 239 | + repo, err := gogit.PlainCloneContext(ctx, checkoutDir, false, opts) |
| 240 | + if err != nil { |
| 241 | + return "", fmt.Errorf("failed to clone: %w", err) |
| 242 | + } |
| 243 | + |
| 244 | + if tc.CheckoutHash != "" { |
| 245 | + workTree, err := repo.Worktree() |
| 246 | + if err != nil { |
| 247 | + return "", err |
| 248 | + } |
| 249 | + opts := &gogit.CheckoutOptions{ |
| 250 | + Hash: plumbing.NewHash(tc.CheckoutHash), |
| 251 | + } |
| 252 | + |
| 253 | + if err := workTree.Checkout(opts); err != nil { |
| 254 | + return "", err |
| 255 | + } |
| 256 | + } |
| 257 | + return filepath.Rel(r.workDir, checkoutDir) |
| 258 | +} |
0 commit comments