Skip to content

Commit e9dcb56

Browse files
committed
Support policies in buf.lock
1 parent 218a19c commit e9dcb56

File tree

2 files changed

+272
-25
lines changed

2 files changed

+272
-25
lines changed

private/bufpkg/bufconfig/buf_lock_file.go

Lines changed: 159 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,23 @@ type BufLockFile interface {
8181
// Files with FileVersionV1Beta1 or FileVersionV1 will not have PluginKeys.
8282
// Only files with FileVersionV2 will have PluginKeys with Digests of DigestTypeP1.
8383
RemotePluginKeys() []bufplugin.PluginKey
84+
// RemotePolicyKeys returns the PolicyKeys representing the remote policies as specified in the buf.lock file.
85+
//
86+
// All PolicyKeys will have unique FullNames.
87+
// PolicyKeys are sorted by FullName.
88+
//
89+
// Files with FileVersionV1Beta1 or FileVersionV1 will not have PolicyKeys.
90+
// Only files with FileVersionV2 will have PolicyKeys with Digests of DigestTypeP1.
91+
RemotePolicyKeys() []bufpolicy.PolicyKey
92+
// RemotePluginKeysForPolicy returns the PluginKeys representing the remote plugins as specified in the buf.lock file
93+
// for the given policy.
94+
//
95+
// All PluginKeys will have unique FullNames.
96+
// PluginKeys are sorted by FullName.
97+
//
98+
// Files with FileVersionV1Beta1 or FileVersionV1 will not have PluginKeys.
99+
// Only files with FileVersionV2 will have PluginKeys with Digests of DigestTypeP1.
100+
RemotePluginKeysForPolicy(bufpolicy.PluginKey) []bufplugin.PluginKey
84101

85102
isBufLockFile()
86103
}
@@ -190,17 +207,21 @@ func BufLockFileWithDigestResolver(
190207
// *** PRIVATE ***
191208

192209
type bufLockFile struct {
193-
fileVersion FileVersion
194-
objectData ObjectData
195-
depModuleKeys []bufmodule.ModuleKey
196-
remotePluginKeys []bufplugin.PluginKey
210+
fileVersion FileVersion
211+
objectData ObjectData
212+
depModuleKeys []bufmodule.ModuleKey
213+
remotePluginKeys []bufplugin.PluginKey
214+
remotePolicyKeys []bufpolicy.PolicyKey
215+
remotePluginKeysForPolicy map[bufpolicy.PluginKey][]bufplugin.PluginKey
197216
}
198217

199218
func newBufLockFile(
200219
fileVersion FileVersion,
201220
objectData ObjectData,
202221
depModuleKeys []bufmodule.ModuleKey,
203222
remotePluginKeys []bufplugin.PluginKey,
223+
remotePolicyKeys []bufpolicy.PolicyKey,
224+
remotePluginKeysForPolicy map[string][]bufplugin.PluginKey,
204225
) (*bufLockFile, error) {
205226
if err := validateNoDuplicateModuleKeysByFullName(depModuleKeys); err != nil {
206227
return nil, err
@@ -216,13 +237,22 @@ func newBufLockFile(
216237
if len(remotePluginKeys) > 0 {
217238
return nil, errors.New("remote plugins are not supported in v1 or v1beta1 buf.lock files")
218239
}
240+
if len(remotePolicyKeys) > 0 || len(remotePluginKeysForPolicy) > 0 {
241+
return nil, errors.New("remote policies are not supported in v1 or v1beta1 buf.lock files")
242+
}
219243
case FileVersionV2:
220244
if err := validateModuleExpectedDigestType(depModuleKeys, fileVersion, bufmodule.DigestTypeB5); err != nil {
221245
return nil, err
222246
}
223247
if err := validatePluginExpectedDigestType(remotePluginKeys, fileVersion, bufplugin.DigestTypeP1); err != nil {
224248
return nil, err
225249
}
250+
if err := validatePolicyExpectedDigestType(remotePolicyKeys, fileVersion, bufpolicy.DigestTypeP1); err != nil {
251+
return nil, err
252+
}
253+
if err := validatePluginKeysForPolicy(remotePluginKeysForPolicy, fileVersion); err != nil {
254+
return nil, err
255+
}
226256
default:
227257
return nil, syserror.Newf("unknown FileVersion: %v", fileVersion)
228258
}
@@ -273,6 +303,17 @@ func (l *bufLockFile) RemotePluginKeys() []bufplugin.PluginKey {
273303
return l.remotePluginKeys
274304
}
275305

306+
func (l *bufLockFile) RemotePolicyKeys() []bufpolicy.PolicyKey {
307+
return l.remotePolicyKeys
308+
}
309+
310+
func (l *bufLockFile) RemotePluginKeysForPolicy(pluginKey bufpolicy.PluginKey) []bufplugin.PluginKey {
311+
if l.remotePluginKeysForPolicy == nil {
312+
return nil
313+
}
314+
return l.remotePluginKeysForPolicy[pluginKey.String()]
315+
}
316+
276317
func (*bufLockFile) isBufLockFile() {}
277318
func (*bufLockFile) isFile() {}
278319
func (*bufLockFile) isFileInfo() {}
@@ -346,7 +387,7 @@ func readBufLockFile(
346387
}
347388
depModuleKeys[i] = depModuleKey
348389
}
349-
return newBufLockFile(fileVersion, objectData, depModuleKeys, nil /* remotePluginKeys */)
390+
return newBufLockFile(fileVersion, objectData, depModuleKeys, nil /* remotePluginKeys */, nil /* remotePolicyKeys */, nil /* remotePluginKeysForPolicy */)
350391
case FileVersionV2:
351392
var externalBufLockFile externalBufLockFileV2
352393
if err := getUnmarshalStrict(allowJSON)(data, &externalBufLockFile); err != nil {
@@ -387,44 +428,92 @@ func readBufLockFile(
387428
}
388429
depModuleKeys[i] = depModuleKey
389430
}
390-
remotePluginKeys := make([]bufplugin.PluginKey, len(externalBufLockFile.Plugins))
391-
for i, plugin := range externalBufLockFile.Plugins {
392-
if plugin.Name == "" {
393-
return nil, errors.New("no plugin name specified")
431+
remotePluginKeys, err := parseRemotePluginDeps(externalBufLockFile.Plugins)
432+
if err != nil {
433+
return nil, err
434+
}
435+
remotePolicyKeys := make([]bufpolicy.PolicyKey, len(externalBufLockFile.Policies))
436+
remotePluginKeysForPolicy := make(map[string][]bufplugin.PluginKey)
437+
for i, policy := range externalBufLockFile.Policies {
438+
if policy.Name == "" {
439+
return nil, errors.New("no policy name specified")
394440
}
395-
pluginFullName, err := bufparse.ParseFullName(plugin.Name)
441+
policyFullName, err := bufparse.ParseFullName(policy.Name)
396442
if err != nil {
397-
return nil, fmt.Errorf("invalid plugin name: %w", err)
443+
return nil, fmt.Errorf("invalid policy name: %w", err)
398444
}
399-
if plugin.Commit == "" {
400-
return nil, fmt.Errorf("no commit specified for plugin %s", pluginFullName.String())
445+
if policy.Commit == "" {
446+
return nil, fmt.Errorf("no commit specified for policy %s", policyFullName.String())
401447
}
402-
if plugin.Digest == "" {
403-
return nil, fmt.Errorf("no digest specified for plugin %s", pluginFullName.String())
448+
if policy.Digest == "" {
449+
return nil, fmt.Errorf("no digest specified for policy %s", policyFullName.String())
404450
}
405-
commitID, err := uuidutil.FromDashless(plugin.Commit)
451+
commitID, err := uuidutil.FromDashless(policy.Commit)
406452
if err != nil {
407453
return nil, err
408454
}
409-
pluginKey, err := bufplugin.NewPluginKey(
410-
pluginFullName,
455+
policyKey, err := bufpolicy.NewpolicyKey(
456+
policyFullName,
411457
commitID,
412-
func() (bufplugin.Digest, error) {
413-
return bufplugin.ParseDigest(plugin.Digest)
458+
func() (bufpolicy.Digest, error) {
459+
return bufpolicy.ParseDigest(policy.Digest)
414460
},
415461
)
416462
if err != nil {
417463
return nil, err
418464
}
419-
remotePluginKeys[i] = pluginKey
465+
remotePolicyKeys[i] = policyKey
466+
// Parse the plugins for this policy.
467+
remotePluginKeys, err := parseRemotePluginDeps(policy.Plugins)
468+
if err != nil {
469+
return nil, fmt.Errorf("invalid plugins for policy %q: %w", policyFullName.String(), err)
470+
}
471+
if len(remotePluginKeys) > 0 {
472+
remotePluginKeysForPolicy[policyKey.String()] = remotePluginKeys
473+
}
420474
}
421-
return newBufLockFile(fileVersion, objectData, depModuleKeys, remotePluginKeys)
475+
return newBufLockFile(fileVersion, objectData, depModuleKeys, remotePluginKeys, remotePolicyKeys, remotePluginKeysForPolicy)
422476
default:
423477
// This is a system error since we've already parsed.
424478
return nil, syserror.Newf("unknown FileVersion: %v", fileVersion)
425479
}
426480
}
427481

482+
func parseRemotePluginDeps(pluginDeps []externalBufLockFileDepV2) ([]bufplugin.PluginKey, error) {
483+
remotePluginKeys := make([]bufplugin.PluginKey, len(pluginDeps))
484+
for i, plugin := range pluginDeps {
485+
if plugin.Name == "" {
486+
return nil, errors.New("no plugin name specified")
487+
}
488+
pluginFullName, err := bufparse.ParseFullName(plugin.Name)
489+
if err != nil {
490+
return nil, fmt.Errorf("invalid plugin name: %w", err)
491+
}
492+
if plugin.Commit == "" {
493+
return nil, fmt.Errorf("no commit specified for plugin %s", pluginFullName.String())
494+
}
495+
if plugin.Digest == "" {
496+
return nil, fmt.Errorf("no digest specified for plugin %s", pluginFullName.String())
497+
}
498+
commitID, err := uuidutil.FromDashless(plugin.Commit)
499+
if err != nil {
500+
return nil, err
501+
}
502+
pluginKey, err := bufplugin.NewPluginKey(
503+
pluginFullName,
504+
commitID,
505+
func() (bufplugin.Digest, error) {
506+
return bufplugin.ParseDigest(plugin.Digest)
507+
},
508+
)
509+
if err != nil {
510+
return nil, err
511+
}
512+
remotePluginKeys[i] = pluginKey
513+
}
514+
return remotePluginKeys, nil
515+
}
516+
428517
func writeBufLockFile(
429518
writer io.Writer,
430519
bufLockFile BufLockFile,
@@ -609,6 +698,45 @@ func validatePluginExpectedDigestType(
609698
return nil
610699
}
611700

701+
func validatePolicyExpectedDigestType(
702+
policyKeys []bufpolicy.PolicyKey,
703+
fileVersion FileVersion,
704+
expectedDigestType bufpolicy.DigestType,
705+
) error {
706+
for _, policyKey := range policyKeys {
707+
digest, err := policyKey.Digest()
708+
if err != nil {
709+
return err
710+
}
711+
if digest.Type() != expectedDigestType {
712+
return fmt.Errorf(
713+
"%s lock files must use digest type %v, but remote policy %s had a digest type of %v",
714+
fileVersion,
715+
expectedDigestType,
716+
policyKey.String(),
717+
digest.Type(),
718+
)
719+
}
720+
}
721+
return nil
722+
}
723+
724+
func validatePluginKeysForPolicy(
725+
pluginKeysForPolicy map[bufplugin.PluginKey][]bufplugin.PluginKey,
726+
fileVersion FileVersion,
727+
expectedDigestType bufplugin.DigestType,
728+
) error {
729+
for policyKey, pluginKeys := range pluginKeysForPolicy {
730+
if err := validateNoDuplicatePluginKeysByFullName(pluginKeys); err != nil {
731+
return fmt.Errorf("duplicate plugins attempted to be added to policy %q: %w", policyKey.String(), err)
732+
}
733+
if err := validatePluginExpectedDigestType(pluginKeys, fileVersion, expectedDigestType); err != nil {
734+
return fmt.Errorf("invalid plugins for policy %q: %w", policyKey.String(), err)
735+
}
736+
}
737+
return nil
738+
}
739+
612740
// externalBufLockFileV1Beta1V1 represents the v1 or v1beta1 buf.lock file,
613741
// which have the same shape.
614742
type externalBufLockFileV1Beta1V1 struct {
@@ -631,9 +759,10 @@ type externalBufLockFileDepV1Beta1V1 struct {
631759

632760
// externalBufLockFileV2 represents the v2 buf.lock file.
633761
type externalBufLockFileV2 struct {
634-
Version string `json:"version,omitempty" yaml:"version,omitempty"`
635-
Deps []externalBufLockFileDepV2 `json:"deps,omitempty" yaml:"deps,omitempty"`
636-
Plugins []externalBufLockFileDepV2 `json:"plugins,omitempty" yaml:"plugins,omitempty"`
762+
Version string `json:"version,omitempty" yaml:"version,omitempty"`
763+
Deps []externalBufLockFileDepV2 `json:"deps,omitempty" yaml:"deps,omitempty"`
764+
Plugins []externalBufLockFileDepV2 `json:"plugins,omitempty" yaml:"plugins,omitempty"`
765+
Policies []externalBufLockFilePolicyV2 `json:"policies,omitempty" yaml:"policies,omitempty"`
637766
}
638767

639768
// externalBufLockFileDepV2 represents a single dep within a v2 buf.lock file.
@@ -644,6 +773,11 @@ type externalBufLockFileDepV2 struct {
644773
Digest string `json:"digest,omitempty" yaml:"digest,omitempty"`
645774
}
646775

776+
type externalBufLockFilePolicyV2 struct {
777+
externalBufLockFileDepV2
778+
Plugins []externalBufLockFileDepV2 `json:"plugins,omitempty" yaml:"plugins,omitempty"`
779+
}
780+
647781
type bufLockFileOptions struct {
648782
digestResolver func(
649783
ctx context.Context,
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright 2020-2025 Buf Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package bufconfig
16+
17+
import (
18+
"bytes"
19+
"strings"
20+
"testing"
21+
22+
"github.com/stretchr/testify/assert"
23+
"github.com/stretchr/testify/require"
24+
)
25+
26+
func TestReadWriteBufLockFileRoundTrip(t *testing.T) {
27+
t.Parallel()
28+
29+
testReadWriteBufLockFileRoundTrip(
30+
t,
31+
// input
32+
`version: v2
33+
`,
34+
// expected output
35+
`version: v2
36+
`,
37+
)
38+
39+
testReadWriteBufLockFileRoundTrip(
40+
t,
41+
// input
42+
`version: v2
43+
deps:
44+
- name: buf.testing/acme/date
45+
commit: ffded0b4cf6b47cab74da08d291a3c2f
46+
digest: b5:24ed4f13925cf89ea0ae0127fa28540704c7ae14750af027270221b737a1ce658f8014ca2555f6f7fcd95ea84e071d33f37f86cc36d07fe0d0963329a5ec2462
47+
- name: buf.testing/acme/extension
48+
commit: b8488077ea6d4f6d9562a337b98259c8
49+
digest: b5:d2c1da8f8331c5c75b50549c79fc360394dedfb6a11f5381c4523592018964119f561088fc8aaddfc9f5773ba02692e6fd9661853450f76a3355dec62c1f57b4
50+
plugins:
51+
- name: buf.testing/acme/plugin
52+
commit: ffded0b4cf6b47cab74da08d291a3c2f
53+
digest: b5:24ed4f13925cf89ea0ae0127fa28540704c7ae14750af027270221b737a1ce658f8014ca2555f6f7fcd95ea84e071d33f37f86cc36d07fe0d0963329a5ec2462
54+
policies:
55+
- name: buf.testing/acme/policy
56+
commit: b8488077ea6d4f6d9562a337b98259c8
57+
digest: b5:24ed4f13925cf89ea0ae0127fa28540704c7ae14750af027270221b737a1ce658f8014ca2555f6f7fcd95ea84e071d33f37f86cc36d07fe0d0963329a5ec2462
58+
plugins:
59+
- name: buf.testing/acme/plugin
60+
commit: ffded0b4cf6b47cab74da08d291a3c2f
61+
digest: b5:24ed4f13925cf89ea0ae0127fa28540704c7ae14750af027270221b737a1ce658f8014ca2555f6f7fcd95ea84e071d33f37f86cc36d07fe0d0963329a5ec2462
62+
`,
63+
// expected output
64+
`version: v2
65+
deps:
66+
- name: buf.testing/acme/date
67+
commit: ffded0b4cf6b47cab74da08d291a3c2f
68+
digest: b5:24ed4f13925cf89ea0ae0127fa28540704c7ae14750af027270221b737a1ce658f8014ca2555f6f7fcd95ea84e071d33f37f86cc36d07fe0d0963329a5ec2462
69+
- name: buf.testing/acme/extension
70+
commit: b8488077ea6d4f6d9562a337b98259c8
71+
digest: b5:d2c1da8f8331c5c75b50549c79fc360394dedfb6a11f5381c4523592018964119f561088fc8aaddfc9f5773ba02692e6fd9661853450f76a3355dec62c1f57b4
72+
plugins:
73+
- name: buf.testing/acme/plugin
74+
commit: ffded0b4cf6b47cab74da08d291a3c2f
75+
digest: b5:24ed4f13925cf89ea0ae0127fa28540704c7ae14750af027270221b737a1ce658f8014ca2555f6f7fcd95ea84e071d33f37f86cc36d07fe0d0963329a5ec2462
76+
policies:
77+
- name: buf.testing/acme/policy
78+
commit: ffded0b4cf6b47cab74da08d291a3c2f
79+
digest: b5:24ed4f13925cf89ea0ae0127fa28540704c7ae14750af027270221b737a1ce658f8014ca2555f6f7fcd95ea84e071d33f37f86cc36d07fe0d0963329a5ec2462
80+
plugins:
81+
- name: buf.testing/acme/plugin
82+
commit: ffded0b4cf6b47cab74da08d291a3c2f
83+
digest: b5:24ed4f13925cf89ea0ae0127fa28540704c7ae14750af027270221b737a1ce658f8014ca2555f6f7fcd95ea84e071d33f37f86cc36d07fe0d0963329a5ec2462
84+
`,
85+
)
86+
87+
}
88+
89+
func testReadBufLockFile(
90+
t *testing.T,
91+
inputBufLockFileData string,
92+
) BufLockFile {
93+
bufLockFile, err := ReadBufLockFile(
94+
t.Context(),
95+
strings.NewReader(testCleanYAMLData(inputBufLockFileData)),
96+
defaultBufPolicyYAMLFileName,
97+
)
98+
require.NoError(t, err)
99+
return bufLockFile
100+
}
101+
102+
func testReadWriteBufLockFileRoundTrip(
103+
t *testing.T,
104+
inputBufLockFileData string,
105+
expectedOutputBufLockFileData string,
106+
) {
107+
bufLockFile := testReadBufLockFile(t, inputBufLockFileData)
108+
buffer := bytes.NewBuffer(nil)
109+
err := WriteBufLockFile(buffer, bufLockFile)
110+
require.NoError(t, err)
111+
outputBufLockData := testCleanYAMLData(buffer.String())
112+
assert.Equal(t, testCleanYAMLData(expectedOutputBufLockFileData), outputBufLockData, "output:\n%s", outputBufLockData)
113+
}

0 commit comments

Comments
 (0)