api/proposal.go (188 lines of code) (raw):
/*
Copyright 2021 The Kubernetes Authors.
Licensed 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 api
import (
"bufio"
"bytes"
"crypto/md5"
"fmt"
"io"
"strings"
"github.com/go-playground/validator/v10"
"github.com/pkg/errors"
"k8s.io/enhancements/pkg/yaml"
)
type Stage string
const (
AlphaStage Stage = "alpha"
BetaStage Stage = "beta"
StableStage Stage = "stable"
)
var ValidStages = []Stage{
AlphaStage,
BetaStage,
StableStage,
}
func (s Stage) IsValid() error {
for _, s2 := range ValidStages {
if s == s2 {
return nil
}
}
return fmt.Errorf("invalid stage: %v, should be one of %v", s, ValidStages)
}
type Status string
const (
ProvisionalStatus Status = "provisional"
ImplementableStatus Status = "implementable"
ImplementedStatus Status = "implemented"
DeferredStatus Status = "deferred"
RejectedStatus Status = "rejected"
WithdrawnStatus Status = "withdrawn"
ReplacedStatus Status = "replaced"
)
var ValidStatuses = []Status{
ProvisionalStatus,
ImplementableStatus,
ImplementedStatus,
DeferredStatus,
RejectedStatus,
WithdrawnStatus,
ReplacedStatus,
}
func (s Status) IsValid() error {
for _, s2 := range ValidStatuses {
if s == s2 {
return nil
}
}
return fmt.Errorf("invalid status: %v, should be one of %v", s, ValidStatuses)
}
type Proposals []*Proposal
func (p *Proposals) AddProposal(proposal *Proposal) {
*p = append(*p, proposal)
}
// TODO(api): json fields are not using consistent casing
type Proposal struct {
ID string `json:"id"`
PRNumber string `json:"prNumber,omitempty"`
Name string `json:"name,omitempty"`
Title string `json:"title" yaml:"title" validate:"required"`
Number string `json:"kepNumber" yaml:"kep-number" validate:"required"`
Authors []string `json:"authors" yaml:",flow" validate:"required"`
OwningSIG string `json:"owningSig" yaml:"owning-sig" validate:"required"`
ParticipatingSIGs []string `json:"participatingSigs" yaml:"participating-sigs,flow,omitempty"`
Reviewers []string `json:"reviewers" yaml:",flow"`
Approvers []string `json:"approvers" yaml:",flow" validate:"required"`
Editor string `json:"editor" yaml:"editor,omitempty"`
CreationDate string `json:"creationDate" yaml:"creation-date"`
LastUpdated string `json:"lastUpdated" yaml:"last-updated"`
Status Status `json:"status" yaml:"status" validate:"required"`
SeeAlso []string `json:"seeAlso" yaml:"see-also,omitempty"`
Replaces []string `json:"replaces" yaml:"replaces,omitempty"`
SupersededBy []string `json:"supersededBy" yaml:"superseded-by,omitempty"`
Stage Stage `json:"stage" yaml:"stage"`
LatestMilestone string `json:"latestMilestone" yaml:"latest-milestone"`
Milestone Milestone `json:"milestone" yaml:"milestone"`
FeatureGates []FeatureGate `json:"featureGates" yaml:"feature-gates"`
DisableSupported bool `json:"disableSupported" yaml:"disable-supported"`
Metrics []string `json:"metrics" yaml:"metrics"`
Filename string `json:"-" yaml:"-"`
Error error `json:"-" yaml:"-"`
Contents string `json:"markdown" yaml:"-"`
}
func (p *Proposal) IsMissingMilestone() bool {
return p.LatestMilestone == ""
}
func (p *Proposal) IsMissingStage() bool {
return p.Stage == ""
}
type Milestone struct {
Alpha string `json:"alpha" yaml:"alpha"`
Beta string `json:"beta" yaml:"beta"`
Stable string `json:"stable" yaml:"stable"`
Deprecated string `json:"deprecated" yaml:"deprecated,omitempty"`
Removed string `json:"removed" yaml:"removed,omitempty"`
}
type FeatureGate struct {
Name string `json:"name" yaml:"name"`
Components []string `json:"components" yaml:"components"`
}
type KEPHandler Parser
// TODO(api): Make this a generic parser for all `Document` types
func (k *KEPHandler) Parse(in io.Reader) (*Proposal, error) {
scanner := bufio.NewScanner(in)
count := 0
metadata := []byte{}
var body bytes.Buffer
for scanner.Scan() {
line := scanner.Text() + "\n"
if strings.Contains(line, "---") {
count++
continue
}
if count == 1 {
metadata = append(metadata, []byte(line)...)
} else {
body.WriteString(line)
}
}
kep := &Proposal{
Contents: body.String(),
}
if err := scanner.Err(); err != nil {
return kep, errors.Wrap(err, "reading file")
}
// this file is just the KEP metadata
if count == 0 {
metadata = body.Bytes()
kep.Contents = ""
}
if err := yaml.UnmarshalStrict(metadata, &kep); err != nil {
k.Errors = append(k.Errors, errors.Wrap(err, "error unmarshalling YAML"))
return kep, errors.Wrap(err, "unmarshalling YAML")
}
if err := k.validateStruct(kep); err != nil {
k.Errors = append(k.Errors, err)
return kep, fmt.Errorf("validating KEP: %w", err)
}
kep.ID = hash(kep.OwningSIG + ":" + kep.Title)
return kep, nil
}
// validateStruct returns an error if the given Proposal has invalid fields
// as defined by struct tags, or nil if there are no invalid fields
func (k *KEPHandler) validateStruct(p *Proposal) error {
v := validator.New()
return v.Struct(p)
}
// validateGroups returns errors for each invalid group (e.g. SIG) in the given
// Proposal, or nil if there are no invalid groups
func (k *KEPHandler) validateGroups(p *Proposal) []error {
var errs []error
validGroups := make(map[string]bool)
for _, g := range k.Groups {
validGroups[g] = true
}
for _, g := range p.ParticipatingSIGs {
if _, ok := validGroups[g]; !ok {
errs = append(errs, fmt.Errorf("invalid participating-sig: %s", g))
}
}
if _, ok := validGroups[p.OwningSIG]; !ok {
errs = append(errs, fmt.Errorf("invalid owning-sig: %s", p.OwningSIG))
}
return errs
}
// Validate returns errors for each reason the given proposal is invalid,
// or nil if it is valid
func (k *KEPHandler) Validate(p *Proposal) []error {
var allErrs []error
if err := k.validateStruct(p); err != nil {
allErrs = append(allErrs, fmt.Errorf("struct-based validation: %w", err))
}
if errs := k.validateGroups(p); errs != nil {
allErrs = append(allErrs, errs...)
}
if err := p.Status.IsValid(); err != nil {
allErrs = append(allErrs, err)
}
if err := p.Stage.IsValid(); err != nil {
allErrs = append(allErrs, err)
}
if p.Status == ImplementedStatus && p.Stage != StableStage {
allErrs = append(allErrs, fmt.Errorf("status:implemented implies stage:stable but found: %v", p.Stage))
}
return allErrs
}
func hash(s string) string {
return fmt.Sprintf("%x", md5.Sum([]byte(s)))
}