blob: 9b4e8a4b0a00b1ec6af6f3cec5bc12eef2770520 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package bufmodule
import (
"context"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
)
import (
"go.uber.org/multierr"
)
import (
"github.com/apache/dubbo-kubernetes/pkg/bufman/bufpkg/bufcheck/bufbreaking/bufbreakingconfig"
"github.com/apache/dubbo-kubernetes/pkg/bufman/bufpkg/bufcheck/buflint/buflintconfig"
"github.com/apache/dubbo-kubernetes/pkg/bufman/bufpkg/bufconfig"
"github.com/apache/dubbo-kubernetes/pkg/bufman/bufpkg/bufmodule/bufmoduleref"
breakingv1 "github.com/apache/dubbo-kubernetes/pkg/bufman/gen/proto/go/breaking/v1"
lintv1 "github.com/apache/dubbo-kubernetes/pkg/bufman/gen/proto/go/lint/v1"
modulev1alpha1 "github.com/apache/dubbo-kubernetes/pkg/bufman/gen/proto/go/module/v1alpha1"
"github.com/apache/dubbo-kubernetes/pkg/bufman/pkg/manifest"
"github.com/apache/dubbo-kubernetes/pkg/bufman/pkg/storage"
)
const (
// DefaultDocumentationPath defines the default path to the documentation file, relative to the root of the module.
DefaultDocumentationPath = "buf.md"
// LicenseFilePath defines the path to the license file, relative to the root of the module.
LicenseFilePath = "LICENSE"
// b3DigestPrefix is the digest prefix for the third version of the digest function.
//
// It is used by the CLI cache and intended to eventually replace b1 entirely.
b3DigestPrefix = "b3"
)
// AllDocumentationPaths defines all possible paths to the documentation file, relative to the root of the module.
var AllDocumentationPaths = []string{
DefaultDocumentationPath,
"README.md",
"README.markdown",
}
// ModuleFile is a module file.
type ModuleFile interface {
bufmoduleref.FileInfo
io.ReadCloser
isModuleFile()
}
// Module is a Protobuf module.
//
// It contains the files for the sources, and the dependency names.
//
// Terminology:
//
// Targets (Modules and ModuleFileSets):
//
// Just the files specified to build. This will either be sources, or will be specific files
// within sources, ie this is a subset of Sources. The difference between Targets and Sources happens
// when i.e. the --path flag is used.
//
// Sources (Modules and ModuleFileSets):
//
// The files with no dependencies. This is a superset of Targets and subset of All.
//
// All (ModuleFileSets only):
//
// All files including dependencies. This is a superset of Sources.
type Module interface {
// TargetFileInfos gets all FileInfos specified as target files. This is either
// all the FileInfos belonging to the module, or those specified by ModuleWithTargetPaths().
//
// It does not include dependencies.
//
// The returned TargetFileInfos are sorted by path.
TargetFileInfos(ctx context.Context) ([]bufmoduleref.FileInfo, error)
// SourceFileInfos gets all FileInfos belonging to the module.
//
// It does not include dependencies.
//
// The returned SourceFileInfos are sorted by path.
SourceFileInfos(ctx context.Context) ([]bufmoduleref.FileInfo, error)
// GetModuleFile gets the source file for the given path.
//
// Returns storage.IsNotExist error if the file does not exist.
GetModuleFile(ctx context.Context, path string) (ModuleFile, error)
// DeclaredDirectDependencies returns the direct dependencies declared in the configuration file.
//
// The returned ModuleReferences are sorted by remote, owner, repository, and reference (if
// present). The returned ModulePins are unique by remote, owner, repository.
//
// This does not include any transitive dependencies, but if the declarations are correct,
// this should be a subset of the dependencies from DependencyModulePins.
//
// TODO: validate that this is a subset? This may mess up construction.
DeclaredDirectDependencies() []bufmoduleref.ModuleReference
// DependencyModulePins gets the dependency ModulePins.
//
// The returned ModulePins are sorted by remote, owner, repository, branch, commit, and then digest.
// The returned ModulePins are unique by remote, owner, repository.
//
// This includes all transitive dependencies.
DependencyModulePins() []bufmoduleref.ModulePin
// Documentation gets the contents of the module documentation file, buf.md and returns the string representation.
// This may return an empty string if the documentation file does not exist.
Documentation() string
// DocumentationPath returns the path to the documentation file for the module.
// Can be one of `buf.md`, `README.md` or `README.markdown`
DocumentationPath() string
// License gets the contents of the module license file, LICENSE and returns the string representation.
// This may return an empty string if the documentation file does not exist.
License() string
// BreakingConfig returns the breaking change check configuration set for the module.
//
// This may be nil, since older versions of the module would not have this stored.
BreakingConfig() *bufbreakingconfig.Config
// LintConfig returns the lint check configuration set for the module.
//
// This may be nil, since older versions of the module would not have this stored.
LintConfig() *buflintconfig.Config
// Manifest returns the manifest for the module (possibly nil).
// A manifest's contents contain a lexicographically sorted list of path names along
// with each path's digest. The manifest also stores a digest of its own contents which
// allows verification of the entire Buf module. In addition to the .proto files in
// the module, it also lists the buf.yaml, LICENSE, buf.md, and buf.lock files (if
// present).
Manifest() *manifest.Manifest
// BlobSet returns the raw data for the module (possibly nil).
// Each blob in the blob set is indexed by the digest of the blob's contents. For
// example, the buf.yaml file will be listed in the Manifest with a given digest,
// whose contents can be retrieved by looking up the corresponding digest in the
// blob set. This allows API consumers to get access to the original file contents
// of every file in the module, which is useful for caching or recreating a module's
// original files.
BlobSet() *manifest.BlobSet
getSourceReadBucket() storage.ReadBucket
// ModuleIdentity returns the ModuleIdentity for the Module, if it was
// provided at construction time via ModuleWithModuleIdentity or ModuleWithModuleIdentityAndCommit.
//
// Note this *can* be nil if we did not build from a named module.
// All code must assume this can be nil.
// nil checking should work since the backing type is always a pointer.
ModuleIdentity() bufmoduleref.ModuleIdentity
// Commit returns the commit for the Module, if it was
// provided at construction time via ModuleWithModuleIdentityAndCommit.
// Note this can be empty.
// This will only be set if ModuleIdentity is set. but may not be set
// even if ModuleIdentity is set, that is commit is optional information
// even if we know what module this file came from.
Commit() string
isModule()
}
// ModuleOption is used to construct Modules.
type ModuleOption func(*module)
// ModuleWithModuleIdentity is used to construct a Module with a ModuleIdentity.
func ModuleWithModuleIdentity(moduleIdentity bufmoduleref.ModuleIdentity) ModuleOption {
return func(module *module) {
module.moduleIdentity = moduleIdentity
}
}
// ModuleWithModuleIdentityAndCommit is used to construct a Module with a ModuleIdentity and commit.
//
// If the moduleIdentity is nil, the commit must be empty, that is it is not valid to have
// a non-empty commit and a nil moduleIdentity.
func ModuleWithModuleIdentityAndCommit(moduleIdentity bufmoduleref.ModuleIdentity, commit string) ModuleOption {
return func(module *module) {
module.moduleIdentity = moduleIdentity
module.commit = commit
}
}
// NewModuleForBucket returns a new Module. It attempts to read dependencies
// from a lock file in the read bucket.
func NewModuleForBucket(
ctx context.Context,
readBucket storage.ReadBucket,
options ...ModuleOption,
) (Module, error) {
return newModuleForBucket(ctx, readBucket, options...)
}
// NewModuleForProto returns a new Module for the given proto Module.
func NewModuleForProto(
ctx context.Context,
protoModule *modulev1alpha1.Module,
options ...ModuleOption,
) (Module, error) {
return newModuleForProto(ctx, protoModule, options...)
}
// NewModuleForManifestAndBlobSet returns a new Module given the manifest and blob set.
func NewModuleForManifestAndBlobSet(
ctx context.Context,
manifest *manifest.Manifest,
blobSet *manifest.BlobSet,
options ...ModuleOption,
) (Module, error) {
return newModuleForManifestAndBlobSet(ctx, manifest, blobSet, options...)
}
// ModuleWithTargetPaths returns a new Module that specifies specific file or directory paths to build.
//
// These paths must exist.
// These paths must be relative to the roots.
// These paths will be normalized and validated.
// These paths must be unique when normalized and validated.
// Multiple calls to this option will override previous calls.
//
// Note that this will result in TargetFileInfos containing only these paths, and not
// any imports. Imports, and non-targeted files, are still available via SourceFileInfos.
func ModuleWithTargetPaths(
module Module,
targetPaths []string,
excludePaths []string,
) (Module, error) {
return newTargetingModule(module, targetPaths, excludePaths, false)
}
// ModuleWithTargetPathsAllowNotExist returns a new Module specifies specific file or directory paths to build,
// but allows the specified paths to not exist.
//
// Note that this will result in TargetFileInfos containing only these paths, and not
// any imports. Imports, and non-targeted files, are still available via SourceFileInfos.
func ModuleWithTargetPathsAllowNotExist(
module Module,
targetPaths []string,
excludePaths []string,
) (Module, error) {
return newTargetingModule(module, targetPaths, excludePaths, true)
}
// ModuleWithExcludePaths returns a new Module that excludes specific file or directory
// paths to build.
//
// Note that this will result in TargetFileInfos containing only the paths that have not been
// excluded and any imports. Imports are still available via SourceFileInfos.
func ModuleWithExcludePaths(
module Module,
excludePaths []string,
) (Module, error) {
return newTargetingModule(module, nil, excludePaths, false)
}
// ModuleWithExcludePathsAllowNotExist returns a new Module that excludes specific file or
// directory paths to build, but allows the specified paths to not exist.
//
// Note that this will result in TargetFileInfos containing only these paths, and not
// any imports. Imports, and non-targeted files, are still available via SourceFileInfos.
func ModuleWithExcludePathsAllowNotExist(
module Module,
excludePaths []string,
) (Module, error) {
return newTargetingModule(module, nil, excludePaths, true)
}
// ModuleResolver resolves modules.
type ModuleResolver interface {
// GetModulePin resolves the provided ModuleReference to a ModulePin.
//
// Returns an error that fufills storage.IsNotExist if the named Module does not exist.
GetModulePin(ctx context.Context, moduleReference bufmoduleref.ModuleReference) (bufmoduleref.ModulePin, error)
}
// NewNopModuleResolver returns a new ModuleResolver that always returns a storage.IsNotExist error.
func NewNopModuleResolver() ModuleResolver {
return newNopModuleResolver()
}
// ModuleReader reads resolved modules.
type ModuleReader interface {
// GetModule gets the Module for the ModulePin.
//
// Returns an error that fulfills storage.IsNotExist if the Module does not exist.
GetModule(ctx context.Context, modulePin bufmoduleref.ModulePin) (Module, error)
}
// NewNopModuleReader returns a new ModuleReader that always returns a storage.IsNotExist error.
func NewNopModuleReader() ModuleReader {
return newNopModuleReader()
}
// ModuleFileSet is a Protobuf module file set.
//
// It contains the files for both targets, sources and dependencies.
//
// TODO: we should not have ModuleFileSet inherit from Module, this is confusing
type ModuleFileSet interface {
// Note that GetModuleFile will pull from All files instead of just Source Files!
Module
// AllFileInfos gets all FileInfos associated with the module, including dependencies.
//
// The returned FileInfos are sorted by path.
AllFileInfos(ctx context.Context) ([]bufmoduleref.FileInfo, error)
isModuleFileSet()
}
// NewModuleFileSet returns a new ModuleFileSet.
func NewModuleFileSet(
module Module,
dependencies []Module,
) ModuleFileSet {
return newModuleFileSet(module, dependencies)
}
// Workspace represents a workspace.
//
// It is guaranteed that all Modules within this workspace have no overlapping file paths.
type Workspace interface {
// GetModule gets the module identified by the given ModuleIdentity.
//
GetModule(moduleIdentity bufmoduleref.ModuleIdentity) (Module, bool)
// GetModules returns all of the modules found in the workspace.
GetModules() []Module
}
// NewWorkspace returns a new module workspace.
//
// The Context is not retained, and is only used for validation during construction.
func NewWorkspace(
ctx context.Context,
namedModules map[string]Module,
allModules []Module,
) (Workspace, error) {
return newWorkspace(
ctx,
namedModules,
allModules,
)
}
// ModuleToProtoModule converts the Module to a proto Module.
//
// This takes all Sources and puts them in the Module, not just Targets.
func ModuleToProtoModule(ctx context.Context, module Module) (*modulev1alpha1.Module, error) {
// these are returned sorted, so there is no need to sort
// the resulting protoModuleFiles afterwards
sourceFileInfos, err := module.SourceFileInfos(ctx)
if err != nil {
return nil, err
}
protoModuleFiles := make([]*modulev1alpha1.ModuleFile, len(sourceFileInfos))
for i, sourceFileInfo := range sourceFileInfos {
protoModuleFile, err := moduleFileToProto(ctx, module, sourceFileInfo.Path())
if err != nil {
return nil, err
}
protoModuleFiles[i] = protoModuleFile
}
// these are returned sorted, so there is no need to sort
// the resulting protoModuleNames afterwards
dependencyModulePins := module.DependencyModulePins()
protoModulePins := make([]*modulev1alpha1.ModulePin, len(dependencyModulePins))
for i, dependencyModulePin := range dependencyModulePins {
protoModulePins[i] = bufmoduleref.NewProtoModulePinForModulePin(dependencyModulePin)
}
var protoBreakingConfig *breakingv1.Config
if module.BreakingConfig() != nil {
protoBreakingConfig = bufbreakingconfig.ProtoForConfig(module.BreakingConfig())
}
var protoLintConfig *lintv1.Config
if module.LintConfig() != nil {
protoLintConfig = buflintconfig.ProtoForConfig(module.LintConfig())
}
protoModule := &modulev1alpha1.Module{
Files: protoModuleFiles,
Dependencies: protoModulePins,
Documentation: module.Documentation(),
DocumentationPath: module.DocumentationPath(),
BreakingConfig: protoBreakingConfig,
LintConfig: protoLintConfig,
License: module.License(),
}
if err := ValidateProtoModule(protoModule); err != nil {
return nil, err
}
return protoModule, nil
}
// ModuleDigestB3 returns the b3 digest for the Module.
//
// To create the module digest (SHA256):
// 1. For every file in the module (sorted lexicographically by path):
// a. Add the file path
// b. Add the file contents
// 2. Add the dependency's module identity and commit ID (sorted lexicographically by commit ID)
// 3. Add the module identity if available.
// 4. Add the module documentation if available.
// 5. Add the module documentation path if available.
// 6. Add the module license if available.
// 7. Add the breaking and lint configurations if available.
// 8. Produce the final digest by URL-base64 encoding the summed bytes and prefixing it with the digest prefix
func ModuleDigestB3(ctx context.Context, module Module) (string, error) {
hash := sha256.New()
// We do not want to change the sort order as the rest of the codebase relies on it,
// but we only want to use commit as part of the sort order, so we make a copy of
// the slice and sort it by commit
for _, dependencyModulePin := range copyModulePinsSortedByOnlyCommit(module.DependencyModulePins()) {
if _, err := hash.Write([]byte(dependencyModulePin.IdentityString() + ":" + dependencyModulePin.Commit())); err != nil {
return "", err
}
}
sourceFileInfos, err := module.SourceFileInfos(ctx)
if err != nil {
return "", err
}
for _, sourceFileInfo := range sourceFileInfos {
if _, err := hash.Write([]byte(sourceFileInfo.Path())); err != nil {
return "", err
}
moduleFile, err := module.GetModuleFile(ctx, sourceFileInfo.Path())
if err != nil {
return "", err
}
if _, err := io.Copy(hash, moduleFile); err != nil {
return "", multierr.Append(err, moduleFile.Close())
}
if err := moduleFile.Close(); err != nil {
return "", err
}
}
if moduleIdentity := module.ModuleIdentity(); moduleIdentity != nil {
if _, err := hash.Write([]byte(moduleIdentity.IdentityString())); err != nil {
return "", err
}
}
if docs := module.Documentation(); docs != "" {
if _, err := hash.Write([]byte(docs)); err != nil {
return "", err
}
}
if docPath := module.DocumentationPath(); docPath != "" && docPath != DefaultDocumentationPath {
if _, err := hash.Write([]byte(docPath)); err != nil {
return "", err
}
}
if license := module.License(); license != "" {
if _, err := hash.Write([]byte(license)); err != nil {
return "", err
}
}
if breakingConfig := module.BreakingConfig(); breakingConfig != nil {
breakingConfigBytes, err := bufbreakingconfig.BytesForConfig(breakingConfig)
if err != nil {
return "", err
}
if _, err := hash.Write(breakingConfigBytes); err != nil {
return "", err
}
}
if lintConfig := module.LintConfig(); lintConfig != nil {
lintConfigBytes, err := buflintconfig.BytesForConfig(lintConfig)
if err != nil {
return "", err
}
if _, err := hash.Write(lintConfigBytes); err != nil {
return "", err
}
}
return fmt.Sprintf("%s-%s", b3DigestPrefix, base64.URLEncoding.EncodeToString(hash.Sum(nil))), nil
}
// ModuleToBucket writes the given Module to the WriteBucket.
//
// This writes the sources and the buf.lock file.
// This copies external paths if the WriteBucket supports setting of external paths.
func ModuleToBucket(
ctx context.Context,
module Module,
writeBucket storage.WriteBucket,
) error {
fileInfos, err := module.SourceFileInfos(ctx)
if err != nil {
return err
}
for _, fileInfo := range fileInfos {
if err := putModuleFileToBucket(ctx, module, fileInfo.Path(), writeBucket); err != nil {
return err
}
}
if docs := module.Documentation(); docs != "" {
moduleDocPath := DefaultDocumentationPath
if docPath := module.DocumentationPath(); docPath != "" {
moduleDocPath = docPath
}
if err := storage.PutPath(ctx, writeBucket, moduleDocPath, []byte(docs)); err != nil {
return err
}
}
if license := module.License(); license != "" {
if err := storage.PutPath(ctx, writeBucket, LicenseFilePath, []byte(license)); err != nil {
return err
}
}
if err := bufmoduleref.PutDependencyModulePinsToBucket(ctx, writeBucket, module.DependencyModulePins()); err != nil {
return err
}
// This is the default version created by bufconfig getters. The versions should be the
// same across lint and breaking configs.
version := bufconfig.V1Version
var breakingConfigVersion string
if module.BreakingConfig() != nil {
breakingConfigVersion = module.BreakingConfig().Version
}
var lintConfigVersion string
if module.LintConfig() != nil {
lintConfigVersion = module.LintConfig().Version
}
// If one of either breaking or lint config is non-nil, then other config will also be non-nil,
// even if a module does not set both configurations. An empty with the correct version
// will be set by the configuration getters.
if breakingConfigVersion != lintConfigVersion {
return fmt.Errorf("breaking config version %q does not match lint config version %q", breakingConfigVersion, lintConfigVersion)
}
if breakingConfigVersion != "" || lintConfigVersion != "" {
version = breakingConfigVersion
}
writeConfigOptions := []bufconfig.WriteConfigOption{
bufconfig.WriteConfigWithModuleIdentity(module.ModuleIdentity()),
bufconfig.WriteConfigWithBreakingConfig(module.BreakingConfig()),
bufconfig.WriteConfigWithLintConfig(module.LintConfig()),
bufconfig.WriteConfigWithVersion(version),
}
return bufconfig.WriteConfig(ctx, writeBucket, writeConfigOptions...)
}
// TargetModuleFilesToBucket writes the target files of the given Module to the WriteBucket.
//
// This does not write the buf.lock file.
// This copies external paths if the WriteBucket supports setting of external paths.
func TargetModuleFilesToBucket(
ctx context.Context,
module Module,
writeBucket storage.WriteBucket,
) error {
fileInfos, err := module.TargetFileInfos(ctx)
if err != nil {
return err
}
for _, fileInfo := range fileInfos {
if err := putModuleFileToBucket(ctx, module, fileInfo.Path(), writeBucket); err != nil {
return err
}
}
return nil
}