blob: 0b868e425dff32832d6d6b7dfb147580caebb237 [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 bufconfig
import (
"bytes"
"context"
"errors"
"fmt"
"text/template"
)
import (
"gopkg.in/yaml.v3"
)
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/bufmodule/bufmoduleref"
"github.com/apache/dubbo-kubernetes/pkg/bufman/pkg/storage"
)
// If this is updated, make sure to update docs.buf.build TODO automate this
const (
exampleName = "buf.build/acme/weather"
// This is only used for `buf mod init`.
tmplDocumentationCommentsData = `{{$top := .}}# This specifies the configuration file version.
#
# This controls the configuration file layout, defaults, and lint/breaking
# rules and rule categories. Buf takes breaking changes seriously in
# all aspects, and none of these will ever change for a given version.
#
# The only valid versions are "v1beta1", "v1".
# This key is required.
version: {{.Version}}
# name is the module name.
{{if .NameUnset}}#{{end}}name: {{.Name}}
# deps are the module dependencies
{{if .DepsUnset}}#{{end}}deps:
{{range $dep := .Deps}}{{if $top.DepsUnset}}#{{end}} - {{$dep}}
{{end}}
# build contains the options for builds.
#
# This affects the behavior of buf build, as well as the build behavior
# for source lint and breaking change rules.
#
# If you want to build all files in your repository, this section can be
# omitted.
build:
# excludes is the list of directories to exclude.
#
# These directories will not be built or checked. If a directory is excluded,
# buf treats the directory as if it does not exist.
#
# All directory paths in exclude must be relative to the directory of
# your buf.yaml. Only directories can be specified, and all specified
# directories must within the root directory.
{{if not .Uncomment}}#{{end}}excludes:
{{if not .Uncomment}}#{{end}} - foo
{{if not .Uncomment}}#{{end}} - bar/baz
# lint contains the options for lint rules.
lint:
# use is the list of rule categories and ids to use for buf lint.
#
# Categories are sets of rule ids.
# Run buf config ls-lint-rules --all to get a list of all rules.
#
# The union of the categories and ids will be used.
#
# The default is [DEFAULT].
use:
{{range $lint_id := .LintConfig.Use}} - {{$lint_id}}
{{end}}
# except is the list of rule ids or categories to remove from the use
# list.
#
# This allows removal of specific rules from the union of rules derived
# from the use list of categories and ids.
{{if not .Uncomment}}#{{end}}except:
{{if not .Uncomment}}#{{end}} - ENUM_VALUE_UPPER_SNAKE_CASE
# ignore is the list of directories or files to ignore for all rules.
#
# All directories and file paths are specified relative to the root directory.
# The directory "." is not allowed - this is equivalent to ignoring
# everything.
{{if not .Uncomment}}#{{end}}ignore:
{{if not .Uncomment}}#{{end}} - bat
{{if not .Uncomment}}#{{end}} - ban/ban.proto
# ignore_only is the map from rule id or category to file or directory to
# ignore.
#
# All directories and file paths are specified relative to the root directory.
# The directory "." is not allowed - this is equivalent to using the "except"
# option.
#
# Note you can generate this section using
# "buf lint --error-format=config-ignore-yaml". The result of this command
# can be copy/pasted here.
{{if not .Uncomment}}#{{end}}ignore_only:
{{if not .Uncomment}}#{{end}} ENUM_PASCAL_CASE:
{{if not .Uncomment}}#{{end}} - foo/foo.proto
{{if not .Uncomment}}#{{end}} - bar
{{if not .Uncomment}}#{{end}} FIELD_LOWER_SNAKE_CASE:
{{if not .Uncomment}}#{{end}} - foo
# enum_zero_value_suffix affects the behavior of the ENUM_ZERO_VALUE_SUFFIX
# rule.
#
# This will result in this suffix being used instead of the default
# "_UNSPECIFIED" suffix.
{{if not .Uncomment}}#{{end}}enum_zero_value_suffix: _UNSPECIFIED
# rpc_allow_same_request_response affects the behavior of the
# RPC_REQUEST_RESPONSE_UNIQUE rule.
#
# This will result in requests and responses being allowed to be the same
# type for a single RPC.
{{if not .Uncomment}}#{{end}}rpc_allow_same_request_response: false
# rpc_allow_google_protobuf_empty_requests affects the behavior of the
# RPC_REQUEST_STANDARD_NAME and RPC_REQUEST_RESPONSE_UNIQUE rules.
#
# This will result in google.protobuf.Empty requests being ignored for
# RPC_REQUEST_STANDARD_NAME, and google.protobuf.Empty requests being allowed
# in multiple RPCs.
{{if not .Uncomment}}#{{end}}rpc_allow_google_protobuf_empty_requests: false
# rpc_allow_google_protobuf_empty_responses affects the behavior of the
# RPC_RESPONSE_STANDARD_NAME and the RPC_REQUEST_RESPONSE_UNIQUE rules.
#
# This will result in google.protobuf.Empty responses being ignored for
# RPC_RESPONSE_STANDARD_NAME, and google.protobuf.Empty responses being
# allowed in multiple RPCs.
{{if not .Uncomment}}#{{end}}rpc_allow_google_protobuf_empty_responses: false
# service_suffix affects the behavior of the SERVICE_SUFFIX rule.
#
# This will result in this suffix being used instead of the default "Service"
# suffix.
{{if not .Uncomment}}#{{end}}service_suffix: Service
# allow_comment_ignores allows comment-driven ignores.
#
# If this option is set, leading comments can be added within Protobuf files
# to ignore lint errors for certain components. If any line in a leading
# comment starts with "buf:lint:ignore ID", then Buf will ignore lint errors
# for this id. For example:
#
# syntax = "proto3";
#
# // buf:lint:ignore PACKAGE_LOWER_SNAKE_CASE
# // buf:lint:ignore PACKAGE_VERSION_SUFFIX
# package A;
#
# We do not recommend using this, as it allows individual engineers in a
# large organization to decide on their own lint rule exceptions. However,
# there are cases where this is necessarily, and we want users to be able to
# make informed decisions, so we provide this as an opt-in.
{{if not .Uncomment}}#{{end}}allow_comment_ignores: false
# breaking contains the options for breaking rules.
breaking:
# use is the list of rule categories and ids to use for
# buf breaking.
#
# Categories are sets of rule ids.
# Run buf config ls-breaking-rules --all to get a list of all rules.
#
# The union of the categories and ids will be used.
#
# As opposed to lint, where you may want to do more customization, with
# breaking is is generally better to only choose one of the following
# options:
#
# - [FILE]
# - [PACKAGE]
# - [WIRE]
# - [WIRE_JSON]
#
# The default is [FILE], as done below.
use:
{{range $breaking_id := .BreakingConfig.Use}} - {{$breaking_id}}
{{end}}
# except is the list of rule ids or categories to remove from the use
# list.
#
# This allows removal of specific rules from the union of rules derived
# from the use list of categories and ids.
#
# As opposed to lint, we generally recommend using one of the preconfigured
# options. Removing specific rules from breaking change detection is an
# advanced option.
{{if not .Uncomment}}#{{end}}except:
{{if not .Uncomment}}#{{end}} - FIELD_SAME_NAME
# ignore is the list of directories or files to ignore for all rules.
#
# All directories and file paths are specified relative to the root directory.
# The directory "." is not allowed - this is equivalent to ignoring
# everything.
{{if not .Uncomment}}#{{end}}ignore:
{{if not .Uncomment}}#{{end}} - bat
{{if not .Uncomment}}#{{end}} - ban/ban.proto
# ignore_only is the map from rule id or category to file or directory to
# ignore.
#
# All directories and file paths are specified relative to a root directory.
# The directory "." is not allowed - this is equivalent to using the "except"
# option.
{{if not .Uncomment}}#{{end}}ignore_only:
{{if not .Uncomment}}#{{end}} FIELD_NO_DELETE:
{{if not .Uncomment}}#{{end}} - foo/foo.proto
{{if not .Uncomment}}#{{end}} - bar
{{if not .Uncomment}}#{{end}} WIRE_JSON:
{{if not .Uncomment}}#{{end}} - foo
# ignore_unstable_packages results in ignoring packages with a last component
# that is one of the unstable forms recognized by the "PACKAGE_VERSION_SUFFIX"
# lint rule. The following forms will be ignored:
#
# - v\d+test.*
# - v\d+(alpha|beta)\d+
# - v\d+p\d+(alpha|beta)\d+
#
# For example, if this option is set, the following packages will be ignored:
#
# - foo.bar.v1alpha1
# - foo.bar.v1beta1
# - foo.bar.v1test
{{if not .Uncomment}}#{{end}}ignore_unstable_packages: false`
)
func writeConfig(
ctx context.Context,
writeBucket storage.WriteBucket,
options ...WriteConfigOption,
) error {
writeConfigOptions := newWriteConfigOptions()
for _, option := range options {
option(writeConfigOptions)
}
if err := validateWriteConfigOptions(writeConfigOptions); err != nil {
return err
}
// This is the same default as the bufconfig getters.
version := V1Version
if writeConfigOptions.version != "" {
if err := ValidateVersion(writeConfigOptions.version); err != nil {
return err
}
version = writeConfigOptions.version
}
config := &Config{
Version: version,
ModuleIdentity: writeConfigOptions.moduleIdentity,
}
var breakingConfigVersion string
breakingConfig := writeConfigOptions.breakingConfig
if breakingConfig != nil {
breakingConfigVersion = breakingConfig.Version
if breakingConfigVersion != version {
return fmt.Errorf("version %q found for breaking config, does not match top level config version: %q", breakingConfigVersion, version)
}
config.Breaking = breakingConfig
}
var lintConfigVersion string
lintConfig := writeConfigOptions.lintConfig
if lintConfig != nil {
lintConfigVersion = lintConfig.Version
if lintConfigVersion != version {
return fmt.Errorf("version %q found for lint config, does not match top level config version: %q", lintConfigVersion, version)
}
config.Lint = lintConfig
}
// We should never hit this condition since we are already validating against `version`.
if breakingConfigVersion != lintConfigVersion {
return fmt.Errorf("breaking config version %q does not match lint config version %q", breakingConfigVersion, lintConfigVersion)
}
var dependencies []string
if len(writeConfigOptions.dependencyModuleReferences) > 0 {
dependencies = make([]string, len(writeConfigOptions.dependencyModuleReferences))
for i, dependencyModuleReference := range writeConfigOptions.dependencyModuleReferences {
dependencies[i] = dependencyModuleReference.String()
}
}
if writeConfigOptions.documentationComments {
// Write out config using template with document comments.
return writeCommentedConfig(ctx, writeBucket, config, dependencies, writeConfigOptions.uncomment)
}
// Write out raw default config without comments using externalConfig{V1, V1Beta1}
return writeExternalConfig(ctx, writeBucket, version, config, dependencies)
}
func writeExternalConfig(
ctx context.Context,
writeBucket storage.WriteBucket,
version string,
config *Config,
dependencies []string,
) error {
var name string
if config.ModuleIdentity != nil {
name = config.ModuleIdentity.IdentityString()
}
breakingConfig := config.Breaking
lintConfig := config.Lint
switch version {
case V1Beta1Version:
var externalBreakingConfig bufbreakingconfig.ExternalConfigV1Beta1
if breakingConfig != nil {
externalBreakingConfig = bufbreakingconfig.ExternalConfigV1Beta1ForConfig(breakingConfig)
}
var externalLintConfig buflintconfig.ExternalConfigV1Beta1
if lintConfig != nil {
externalLintConfig = buflintconfig.ExternalConfigV1Beta1ForConfig(lintConfig)
}
externalConfig := ExternalConfigV1Beta1{
Name: name,
Version: config.Version,
Deps: dependencies,
Breaking: externalBreakingConfig,
Lint: externalLintConfig,
}
buffer := bytes.NewBuffer(nil)
encoder := yaml.NewEncoder(buffer)
encoder.SetIndent(2)
if err := encoder.Encode(externalConfig); err != nil {
return err
}
return storage.PutPath(ctx, writeBucket, ExternalConfigV1Beta1FilePath, buffer.Bytes())
case V1Version:
var externalBreakingConfig bufbreakingconfig.ExternalConfigV1
if breakingConfig != nil {
externalBreakingConfig = bufbreakingconfig.ExternalConfigV1ForConfig(breakingConfig)
}
var externalLintConfig buflintconfig.ExternalConfigV1
if lintConfig != nil {
externalLintConfig = buflintconfig.ExternalConfigV1ForConfig(lintConfig)
}
externalConfig := ExternalConfigV1{
Name: name,
Version: config.Version,
Deps: dependencies,
Breaking: externalBreakingConfig,
Lint: externalLintConfig,
}
buffer := bytes.NewBuffer(nil)
encoder := yaml.NewEncoder(buffer)
encoder.SetIndent(2)
if err := encoder.Encode(externalConfig); err != nil {
return err
}
return storage.PutPath(ctx, writeBucket, ExternalConfigV1FilePath, buffer.Bytes())
default:
return fmt.Errorf(`%s has an invalid "version %s"`, name, version)
}
}
type tmplParam struct {
Uncomment bool
Version string
Name string
NameUnset bool
Deps []string
DepsUnset bool
LintConfig *buflintconfig.Config
BreakingConfig *bufbreakingconfig.Config
}
func newTmplParam(config *Config, dependencies []string, uncomment bool) *tmplParam {
var name string
var nameUnset bool
if config.ModuleIdentity != nil {
name = config.ModuleIdentity.IdentityString()
}
if name == "" {
name = exampleName
nameUnset = true
}
var dependenciesUnset bool
if len(dependencies) == 0 {
dependencies = []string{
"buf.build/acme/petapis",
"buf.build/acme/pkg:alpha",
"buf.build/acme/paymentapis:7e8b594e68324329a7aefc6e750d18b9",
}
dependenciesUnset = true
}
return &tmplParam{
Uncomment: uncomment,
Version: config.Version,
Name: name,
NameUnset: nameUnset,
Deps: dependencies,
DepsUnset: dependenciesUnset,
LintConfig: config.Lint,
BreakingConfig: config.Breaking,
}
}
func writeCommentedConfig(
ctx context.Context,
writeBucket storage.WriteBucket,
config *Config,
dependencies []string,
uncomment bool,
) error {
tmplData := tmplDocumentationCommentsData
tmpl, err := template.New("tmpl").Parse(tmplData)
if err != nil {
return err
}
buffer := bytes.NewBuffer(nil)
if err := tmpl.Execute(
buffer,
newTmplParam(
config,
dependencies,
uncomment,
),
); err != nil {
return err
}
return storage.PutPath(ctx, writeBucket, ExternalConfigV1FilePath, buffer.Bytes())
}
type writeConfigOptions struct {
documentationComments bool
uncomment bool
moduleIdentity bufmoduleref.ModuleIdentity
dependencyModuleReferences []bufmoduleref.ModuleReference
breakingConfig *bufbreakingconfig.Config
lintConfig *buflintconfig.Config
version string
}
func newWriteConfigOptions() *writeConfigOptions {
return &writeConfigOptions{}
}
func validateWriteConfigOptions(writeConfigOptions *writeConfigOptions) error {
if !writeConfigOptions.documentationComments && writeConfigOptions.uncomment {
return errors.New("cannot set uncomment without documentationComments for WriteConfig")
}
if writeConfigOptions.moduleIdentity == nil && len(writeConfigOptions.dependencyModuleReferences) > 0 {
return errors.New("cannot set deps without a name for WriteConfig")
}
return nil
}